Гид по технологиям

Как создать клон Hacker News на React

9 min read React Обновлено 30 Dec 2025
Клон Hacker News на React — руководство
Клон Hacker News на React — руководство

Логотип React на фоне человека, использующего ноутбук и мобильный телефон

Введение

Hacker News — популярный сайт среди предпринимателей и разработчиков с контентом про компьютерные науки и стартапы. Его простая верстка удобна многим, но если вы хотите более современный или персонализированный интерфейс, вы можете создать собственный клон, получая данные из открытых API. Разработка такого клона помогает закрепить навыки React и освоить работу с внешними API, маршрутизацией и рекурсивной отрисовкой комментариев.

Важно: в этом руководстве предполагается базовое знание JavaScript и React. Если вы не знакомы с понятиями «SPA» или «хуки», ниже есть краткая глоссарная строка.

Что вы получите

  • Рабочее React-приложение с маршрутизацией (React Router).
  • Пользовательский хук useFetch для работы с двумя API.
  • Страницы списка и отдельной записи с комментариями и вложенными ответами.
  • Навигацию по категориям: Home, Best, Show, Ask, Jobs.
  • Контроль загрузки, ошибок и состояния.

Важно: код в репозитории распространяется по MIT-лицензии.

Необходимые пакеты и инструменты

  • Vite для генерации проекта.
  • React Router для маршрутизации SPA.
  • html-react-parser для парсинга HTML, который возвращают API.
  • moment для форматирования дат в относительном виде.

Пример команд (используется yarn; можно заменить на npm):

        `yarn create vite  
`
    

Затем внутри созданного проекта выполните:

        `yarn add html-react-parser  
yarn add react-router-dom  
yarn add moment  
yarn dev  
`
    

Если вы предпочитаете npm, замените команды на npm init vite@latest и npm install.

Структура проекта

Создайте в 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 оставьте минимальную функциональную компоненту:

        `function App() {  
  return (  
    <>  
      
  )  
}  
  
export default App  
`
    

Импортируйте модули для маршрутов и добавьте Routes с тремя путями: /, /:type, /item/:id.

        `import { Routes, Route } from 'react-router-dom'  
import ListPage from './pages/ListPage'  
import Navbar from './components/Navbar'  
import PostPage from './pages/PostPage'  
`
    

Вставьте Routes в фрагмент, чтобы рендерить Navbar и ListPage для корня и категорий, а PostPage — для страницы элемента:

        `  
     }>  
     
     }>  
     
    }>  
     
  
`
    

Создание пользовательского хука useFetch

Hook useFetch отвечает за получение данных из двух API:

  • node-hnapi.herokuapp.com — возвращает списки постов по типу (news, best, show и т.д.).
  • hn.algolia.com/api/v1/items — возвращает конкретную запись с комментариями.

Откройте src/hooks/useFetch.jsx и опишите хук как экспорт по умолчанию, импортировав useState и useEffect:

        `import { useState, useEffect } from "react";  
export default function useFetch(type, id) {  
  
}  
`
    

Определите состояния data, error и loading:

        `const [data, setData] = useState();  
const [error, setError] = useState(false);  
const [loading, setLoading] = useState(true);  
`
    

Добавьте useEffect с зависимостями id и type и функцию fetchData, которая переключается между API в зависимости от аргументов:

        `useEffect(() => {  
}, [id, type])  
`
    

Реализация fetchData в примере:

        `async function fetchData() {  
    let response, url, parameter;  
    if (type) {   
       url = "https://node-hnapi.herokuapp.com/";   
       parameter = type.toLowerCase();   
   }  
    else if (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 };  
`
    

Советы по надёжности: оборачивайте fetch в контролируемую отмену (abort controller) при необходимости, особенно если компонента может размонтироваться прежде, чем придёт ответ.

Отрисовка списка постов

ListPage отвечает за показ списка постов в зависимости от категории (type). Импортируйте useNavigate и useParams, используйте useFetch для получения данных.

        `import { useNavigate, useParams } from "react-router-dom";  
import useFetch from "../hooks/useFetch";  
`
    

В компоненте назначьте type из параметров маршрута, по умолчанию — news, затем вызовите хук:

        `export default function ListPage() {  
    let { type } = useParams();  
    const navigate = useNavigate();  
    if (!type) type = "news";  
    const { loading, error, data } = useFetch(type, null);  
}  
`
    

В зависимости от состояния верните соответствующий JSX:

        `if (error) {  
    return 
Something went wrong!
} if (loading) {     return 
Loading
} 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-подсказка: укажите document.title, чтобы вкладка отражала текущую категорию или заголовок поста.

Страница поста

PostPage показывает детальную страницу записи и комментарии. Импортируйте Link, useParams, parse, moment, Comments и 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";  
  
export default function PostPage() {  
    const { id } = useParams();  
    const { loading, error, data } = useFetch(null, id);  
}  
`
    

Рендер по состоянию — аналогично ListPage:

        `if (error) {  
    return 
Something went wrong!
} if (loading) {     return 
Loading
} 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
                     
    
} `

Замечание: parse безопасно парсит HTML-контент, но будьте внимательны к возможным XSS: html-react-parser не выполняет автоматическую санитацию, поэтому в чувствительных приложениях добавьте процесс очистки.

Компонент комментариев и вложенные ответы

Comments отвечает за обход массива commentsData и рекурсивную отрисовку Node для каждого комментария:

        `import parse from 'html-react-parser';  
import moment from "moment";  
  
export default function Comments({ commentsData }) {  
    return <>  
        {commentsData.map(commentData => )}  
      
}  
`
    

Node рендерит текст комментария, метаданные и рекурсивно свои ответы:

        `function Node({ commentData }) {  
    return   
        {  
          commentData.text &&  
            <>  
                
                    {commentData.author}                                              {moment(commentData.created_at).fromNow()}                                     
                
               {parse(commentData.text)}
                     }         
       {(commentData.children) &&        commentData.children.map(child =>        )}        
    
} `

Примечание: рекурсия должна быть контролируема — глубоко вложенные комментарии могут привести к большому DOM. Рассмотрите виртуализацию или ленивую подгрузку при необходимости.

Компонент Navbar

Navbar использует NavLink из react-router-dom и предоставляет навигацию по категориям:

        `import { NavLink } from "react-router-dom"  
  
export default function Navbar() {  
    return   
}  
`
    

Скриншот готового клона

ALT: Скриншот интерфейса готового клона Hacker News — список постов и панель навигации.

Стилизация

Скопируйте содержимое index.css из репозитория в ваш src/index.css. Важно поддерживать простую и читабельную типографику, а также доступность (контраст, фокусные состояния для ссылок и кнопок).

Советы по локализации UI: переведите статические метки (Home, Best, Comments) если сайт будет использоваться на русском языке.

Расширения и идеи для улучшения

  • Поиск по постам и пользователям через Algolia или встроенный индекс.
  • Локальное кеширование (IndexedDB или localStorage) для оффлайн-доступа.
  • Пагинация и виртуализация списков для производительности.
  • Реализация тёмной темы и переключателя тем.
  • Компонент для рейтинга/лайков, если вы храните данные на своём сервере.

Безопасность и производительность

  • Санитизируйте HTML перед выводом, если ввод не полностью доверен.
  • Используйте React.StrictMode в разработке, но тестируйте поведение и выключайте лишние логирования в продакшене.
  • Добавьте debounce на поисковые запросы и кеширование ответов API.
  • Для большого количества комментариев рассмотрите ленивую подгрузку (load more) для ответов.

Критерии приёмки

  1. При старте приложения маршрут / показывает список новостей (news).
  2. Переход по NavLink корректно меняет категорию и URL.
  3. Клик по заголовку поста открывает /item/:id с полной информацией и комментариями.
  4. Страницы корректно обрабатывают состояния загрузки и ошибок.
  5. HTML в теле поста и комментариях корректно парсится и отображается.

Тест-кейсы и проверка

  • Открыть / и убедиться, что отображается не пустой список.
  • Переключиться на /best, /show, /ask, /jobs и проверить обновление данных.
  • Открыть произвольный пост, проверить авторство, ссылку на сайт и список комментариев.
  • Имитировать отказ сети и убедиться, что отображается сообщение об ошибке.

Роль‑в‑зоне — чек-листы

Разработчик:

  • Настроил Vite и установил зависимости.
  • Реализовал useFetch с обработкой ошибок.
  • Сделал рендер ListPage и PostPage.

Код‑ревьюер:

  • Проверил обработку краевых случаев (пустые данные, ошибка сети).
  • Проверил отсутствие утечек памяти (fetch + отмена при размонтировании).

Продакт‑менеджер:

  • Подтвердил список категорий и UX навигации.
  • Утвердил критерии приёмки.

Мини‑методология разработки

  1. Прототип: реализовать минимальный путь — список -> пост -> комментарии.
  2. Тестирование: ручные сценарии и unit-тесты для хуков и утилит.
  3. Оптимизация: измерить время ответа и уменьшить лишние рендеры.
  4. Развертывание: собрать статический бандл и разместить на Netlify/Vercel/GitHub Pages.

Decision flowchart (выбор API) в виде диаграммы

flowchart TD
  A[Запрос данных] --> B{Есть параметр type?}
  B -- Да --> C[GET https://node-hnapi.herokuapp.com/:type]
  B -- Нет --> D{Есть параметр id?}
  D -- Да --> E[GET https://hn.algolia.com/api/v1/items/:id]
  D -- Нет --> F[Ошибка: недостаточно параметров]

Ментальные модели и подсказки

  • Разделяйте представление и данные: хуки — для получения данных, компоненты — для отображения.
  • Думайте об API как о слое, который может быть изменён: инкапсулируйте URL и логику парсинга в одном месте.
  • При рекурсивной отрисовке контролируйте глубину и размер DOM.

Локализация и особенности для русскоязычной аудитории

  • Переведите статичные строки интерфейса и сообщения об ошибках.
  • Формат даты: moment поддерживает локали; установите moment.locale(‘ru’) для русских относительных дат.
  • Проверьте отображение длинных заголовков и перенос слов для кириллицы.

Тонкое улучшение UX

  • Показывайте skeleton‑загрузку вместо простого «Loading». Это повышает восприятие скорости.
  • Для ссылок наружу ставьте иконку «внешняя ссылка» и предупреждение, что пользователь покидает сервис.

Короткое объявление проекта (100–200 слов)

Вы собрали фронтенд‑клиент для Hacker News на React: современное SPA с навигацией по категориям, страницей записи и рекурсивной секцией комментариев. Проект использует Vite, React Router, html-react-parser и moment. Код доступен в репозитории под MIT‑лицензией. Это отличная база для экспериментов: добавьте поиск, кеширование, темы и авторизацию, чтобы превратить клон в персонализированное приложение для чтения технических новостей.

Social preview

OG title: Клон Hacker News на React — пошагово OG description: Пошаговое руководство по созданию SPA-клона Hacker News на React с хуком useFetch, маршрутизацией и парсингом комментариев.

Короткое резюме

  • Построение клона Hacker News — хорошая практика для освоения React и работы с API.
  • Главные части: роутинг, пользовательский хук, рендер списка и рекурсивные комментарии.
  • Обратите внимание на безопасность (санитизация HTML) и производительность (виртуализация списков).

Важное:

  • Всегда проверяйте происхождение HTML перед вставкой в DOM.

Краткие шаги для запуска локально:

  1. yarn create vite
  2. yarn add html-react-parser react-router-dom moment
  3. yarn dev

Надеюсь, это руководство поможет вам быстро собрать рабочий клиент Hacker News и даст идеи для дальнейшего развития проекта.

1-строчный глоссарий:

  • SPA: single-page application, одностраничное веб‑приложение.
  • Hook: функция React для работы со стейтом и эффектами.
  • Algolia: поисковый API, используемый здесь для получения деталей поста и комментариев.
Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Как продлить заряд и срок службы батареи
Мобильные устройства

Как продлить заряд и срок службы батареи

Gmail как приложение для рабочего стола
Почта

Gmail как приложение для рабочего стола

Как сохранить Mac прохладным в жару
Mac

Как сохранить Mac прохладным в жару

Astro + Nano Stores: управление состоянием
Веб-разработка

Astro + Nano Stores: управление состоянием

Отключить «Не беспокоить» в Google Maps при вождении
Android.

Отключить «Не беспокоить» в Google Maps при вождении

SSH-ключи: создать и установить на сервер
Безопасность

SSH-ключи: создать и установить на сервер