Клон Hacker News на React — пошаговое руководство
Создайте свой клиент Hacker News на React — проект объясняет, как настроить Vite, подключить маршрутизацию, написать кастомный хук useFetch для двух API, отрисовать список постов, страницу поста и вложенные комментарии. В статье также собраны улучшения, чек-листы и критерии приёмки для полноценной SPA.
Почему стоит клонировать Hacker News
- Практическая задача для закрепления React-концепций: маршрутизация, кастомные хуки, работа с REST/Algolia API, рекурсивные компоненты.
- Проект компактный, но покрывает типичные паттерны SPA, которые пригодятся в реальных приложениях.
- Легко расширяем: поиск, кэширование, серверный рендеринг, аутентификация.
Важно: этот материал объясняет фронтендную часть клиента — данные берутся из публичных API.
Установка проекта и запуск сервера разработки
Код проекта доступен в репозитории на GitHub под MIT-лицензией. Для стилизации скопируйте содержимое файла index.css из репозитория в ваш локальный index.css. Есть демо-версия проекта, если хотите посмотреть результат вживую.
Нужные пакеты:
- react-router-dom — маршрутизация для SPA.
- html-react-parser — парсинг HTML, возвращаемого API (комментарии, текст).
- moment — работа с датами (человеко-понятные относительные времена).
Откройте терминал и выполните:
yarn create vite
Можно использовать npm вместо yarn при желании. Команда создаст каркас проекта Vite. Дайте проекту имя, выберите React и вариант JavaScript.
Зайдите в папку проекта и установите зависимости:
yarn add html-react-parser
yarn add react-router-dom
yarn add moment
yarn dev
После установки и запуска dev-сервера откройте проект в редакторе и создайте в папке src три каталога: components, hooks, pages.
В components: добавьте Comments.jsx и Navbar.jsx. В hooks: useFetch.jsx. В pages: ListPage.jsx и PostPage.jsx.
Удалите App.css и замените содержимое main.jsx на следующее (сохраните синтаксис):
import React from'react'
import { BrowserRouter } from'react-router-dom'
import ReactDOM from'react-dom/client'
import App from'./App.jsx'
import'./index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
,
)
В App.jsx оставьте минимальный функциональный компонент:
functionApp() {
return (
<>
>
)
}
exportdefault App
Импортируйте модули маршрутизации и страницы в App.jsx:
import { Routes, Route } from'react-router-dom'
import ListPage from'./pages/ListPage'
import Navbar from'./components/Navbar'
import PostPage from'./pages/PostPage'
Внутри React-фрагмента добавьте Routes с тремя маршрутами: /, /:type, /item/:id:
>}>
>}>
}>
Совет по удобству разработки: используйте расширение редактора для авто-форматирования и линтинга, чтобы поддерживать единый стиль кода.
Создание кастомного хука useFetch
Проект использует два API:
- node-hnapi.herokuapp.com — возвращает списки постов по категориям (news, best, show, ask, jobs).
- hn.algolia.com — возвращает подробности поста и дерево комментариев.
Создайте файл useFetch.jsx и экспортируйте хук по умолчанию. Импортируйте useState и useEffect.
import { useState, useEffect } from"react";
exportdefaultfunctionuseFetch(type, id) {
}
Определите состояния data, error, loading:
const [data, setData] = useState();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(true);
Добавьте useEffect с зависимостями id и type:
useEffect(() => {
}, [id, type])
Внутри эффекта реализуйте fetchData, выбирая URL и параметр в зависимости от переданных аргументов:
asyncfunctionfetchData() {
let response, url, parameter;
if (type) {
url = "https://node-hnapi.herokuapp.com/";
parameter = type.toLowerCase();
}
elseif (id) {
url = "https://hn.algolia.com/api/v1/items/";
parameter = id.toLowerCase();
}
try {
response = await fetch(`${url}${parameter}`);
} catch (error) {
setError(true);
}
if (response) if (response.status !== 200) {
setError(true);
} else {
let data = await response.json();
setLoading(false);
setData(data);
}
}
fetchData();
Наконец, верните объект { loading, error, data }:
return { loading, error, data };
Рекомендация по улучшению: добавьте AbortController для отмены незавершённых запросов при размонтировании компонента, и минимальное кеширование, чтобы снизить число сетевых запросов.
Отрисовка списка постов в зависимости от категории
Компонент ListPage должен рендериться для маршрутов / и /:type.
Импортируйте модули и хук:
import { useNavigate, useParams } from"react-router-dom";
import useFetch from"../hooks/useFetch";
Определите компонент, получите параметр type и вызовите useFetch:
exportdefaultfunctionListPage() {
let { type } = useParams();
const navigate = useNavigate();
if (!type) type = "news";
const { loading, error, data } = useFetch(type, null);
}
Возвратите JSX в зависимости от состояния:
if (error) {
returnSomething went wrong!
}
if (loading) {
returnLoading
}
if (data) {
document.title = type.toUpperCase();
return
{type}
{data.map(item =>
navigate(`/item/${item.id}`)}>
{item.title}
{item.domain &&
open(`${item.url}`)}>
({item.domain})}
)}
}
UX-совет: добавьте индикатор пагинации или lazy-load для длинных списков и aria-атрибуты для доступности.
Компонент PostPage
Импортируйте нужные пакеты и компоненты, получите id из параметров и вызовите useFetch:
import { Link, useParams } from"react-router-dom";
import parse from'html-react-parser';
import moment from"moment";
import Comments from"../components/Comments";
import useFetch from"../hooks/useFetch";
exportdefaultfunctionPostPage() {
const { id } = useParams();
const { loading, error, data } = useFetch(null, id);
}
Рендерьте разные состояния так же, как в ListPage:
if (error) {
returnSomething went wrong!
}
if (loading) {
returnLoading
}
if (data) {
document.title=data.title;
return
{data.title}
{data.url &&
Visit Website}
{data.author}
{moment(data.created_at).fromNow()}
{data.text &&
{parse(data.text)}}
Comments
}
Совет: проверяйте presence полей (data?.children), чтобы избежать ошибок, если API вернул неожидаемые данные.
Рендеринг комментариев с вложенными ответами
Импортируйте parse и moment. Компонент Comments принимает массив commentsData и рендерит Node для каждого элемента.
import parse from'html-react-parser';
import moment from"moment";
exportdefaultfunctionComments({ commentsData }) {
return<>
{commentsData.map(commentData => )}
>
}
Под компонентом Comments определите Node, который рекурсивно рендерит самого себя для детей (children):
functionNode({ commentData }) {
return
{
commentData.text &&
<>
{commentData.author}
{moment(commentData.created_at).fromNow()}
{parse(commentData.text)}
>
}
{(commentData.children) &&
commentData.children.map(child =>
)}
Заметка: parse безопасен для HTML, но будьте внимательны с пользовательским вводом в других проектах — проверяйте XSS-риски.
Навигация
Откройте Navbar.jsx, импортируйте NavLink из react-router-dom и верните nav с пятью ссылками на категории:
import { NavLink } from"react-router-dom"
exportdefaultfunctionNavbar() {
return
}
Поздравляем — вы собрали клиент Hacker News!
Как закрепить навык: идеи для улучшений
- Поиск и автодополнение: подключите Algolia search API для быстрого поиска по заголовкам и пользователям.
- Кэширование и рефетчинг: используйте React Query / SWR для более надежного кэширования и обновления данных.
- TypeScript: добавьте типизацию для безопасности и автодополнения.
- Тестирование: добавьте unit и интеграционные тесты (Jest + React Testing Library).
- SSR/ISR: для SEO можно попробовать Next.js и подгрузку первых страниц на сервере.
- Доступность: aria-метки, фокусная навигация, контраст цветов.
Когда этот подход не подходит
- Если вам нужна сложная серверная логика (аутентификация, write-операции), одной клиентской реализации недостаточно.
- Если проект требует строгой доступности и SEO с первого рендера — подумайте о серверном рендеринге.
- Если ожидается большой трафик и высокая нагрузка API — полезно добавить слой прокси/кэша на сервере.
Чек-листы по ролям
Чек-лист для разработчика:
- Запущен dev-сервер (yarn dev).
- Установлены все зависимости (html-react-parser, react-router-dom, moment).
- Созданы папки components/hooks/pages.
- Хук useFetch возвращает loading/error/data.
- Маршруты корректно работают: /, /best, /show, /ask, /jobs, /item/:id.
- Комментарии корректно рендерятся рекурсивно.
Чек-лист для code-review:
- Нет утечек памяти (очистка эффектов, AbortController).
- Обработка ошибок и состояния загрузки есть для всех async операций.
- Компоненты простые, соблюдён один уровень ответственности.
- Локализация дат/времени (если требуется) учтена.
Критерии приёмки
- При заходе на / появляется список новостей (news).
- При переходе на /best, /show, /ask, /jobs список обновляется согласно типу.
- При клике на заголовок происходит переход на /item/:id и отображается страница поста.
- Комментарии отображаются с вложенностью и временем в формате «N часов/дней назад».
- В случае ошибки сети пользователь видит понятное сообщение об ошибке.
Шпаргалка команд и зависимостей
- Инициализация Vite: yarn create vite
- Установка зависимостей: yarn add html-react-parser react-router-dom moment
- Запуск dev-сервера: yarn dev
Мини-список импортов, которые вы будете часто использовать:
- import parse from ‘html-react-parser’
- import moment from ‘moment’
- import { BrowserRouter, Routes, Route, NavLink, useParams, useNavigate } from ‘react-router-dom’
Тестовые сценарии и критерии приёма
- Загрузка списка новостей
- Шаги: открыть /
- Ожидается: показывает список постов, document.title = NEWS
- Переход в другую категорию
- Шаги: открыть /best
- Ожидается: список обновлён под best, ссылки работают
- Открытие поста
- Шаги: кликнуть заголовок
- Ожидается: открывается /item/:id, отображается заголовок, автор, время, текст и комментарии
- Обработка ошибки сети
- Шаги: отключить интернет или замокать fetch с ошибкой
- Ожидается: показывается сообщение “Something went wrong!” (лучше заменить на русскую локализацию в проде)
Ментальные модели и советы по архитектуре
- Single Responsibility: каждый компонент отвечает за одну вещь — ListPage только за список, PostPage только за пост и вложенные комментарии.
- Data-first: хук useFetch инкапсулирует получение данных — изменение источника данных (например, переход на GraphQL) требует правок только в хуке.
- UI-as-state: состояния loading/error/data полностью определяют, что показывать.
Факт-бокс
- Проект охватывает основные аспекты SPA: маршрутизация, загрузка данных, рекурсивные компоненты.
- Алгоритм рендеринга комментариев реализован рекурсивно — эффективен для глубоких деревьев.
Превью для социальных сетей
OG title: Клон Hacker News на React — руководство OG описание: Пошаговое руководство по созданию клиента Hacker News: Vite, маршрутизация, useFetch, комментарии и идеи для улучшения.
Короткое объявление (100–200 слов)
Постройте свой собственный клиент Hacker News на React: быстрое пошаговое руководство показывает, как собрать SPA с Vite, react-router, кастомным хуком useFetch и рекурсивной отрисовкой комментариев через html-react-parser и moment. В статье есть готовая структура проекта, рабочие компоненты ListPage и PostPage, а также предложения по улучшению (кэширование, TypeScript, тестирование). Этот проект идеально подходит для изучения типовых паттернов фронтенд-разработки и подготовки портфолио.
Важно
- Проверьте CORS и ограничения публичных API — в продакшене может понадобиться прокси.
- Внедряйте защиту от XSS при парсинге HTML вне контролируемых источников.
Резюме
- Постройте минимальную версию по шагам выше.
- Расширяйте через проверенные библиотеки (React Query, TypeScript).
- Пропишите критерии приёмки и тесты перед деплоем.
Краткие выводы
- Проект даёт практику по маршрутизации, хукам и рекурсивным компонентам.
- Легко масштабируется добавлением поиска, кэширования и типизации.
Похожие материалы