Как использовать ванильный класс в React: компонент, хук или сервис?

    При переходе с ванильного JavaScript на React часто возникает вопрос: как перенести существующий класс, который хранит состояние и вспомогательные методы для работы с DOM и историей, в новую экосистему. Рассмотрим основные подходы, их плюсы и минусы, чтобы вы могли выбрать оптимальное решение для вашего SPA.

    Вариант 1: Класс как компонент верхнего уровня

    Самый прямолинейный способ - сделать ваш класс React-компонентом, который будет оборачивать всё приложение. Внутри него можно использовать методы жизненного цикла (componentDidMount, componentWillUnmount) для работы с DOM и историей, а дочерние компоненты (children) будут получать необходимые данные через props или контекст.

    Однако этот подход может привести к созданию «толстого» компонента, который сложно тестировать и поддерживать. Кроме того, классовые компоненты в React постепенно вытесняются функциональными с хуками, поэтому такой вариант считается устаревшим для новых проектов.

    Вариант 2: Пользовательский хук (custom hook)

    Более современный и гибкий способ - вынести логику вашего класса в пользовательский хук. Вы создаёте функцию, которая использует встроенные хуки (useState, useEffect, useRef) для управления состоянием и побочными эффектами. Затем этот хук можно вызывать в любом функциональном компоненте, получая доступ к методам и данным.

    Например, если класс управлял историей переходов, вы можете создать хук useHistoryManager, который будет подписываться на изменения popstate и предоставлять методы навигации. Это делает код переиспользуемым и легко тестируемым.

    Пример структуры хука

    function useHistoryManager(initialState) {\n  const [history, setHistory] = useState(initialState);\n  useEffect(() => {\n    const handlePopState = (event) => { /* обновление истории */ };\n    window.addEventListener('popstate', handlePopState);\n    return () => window.removeEventListener('popstate', handlePopState);\n  }, []);\n  const push = (state) => { /* ... */ };\n  return { history, push };\n}

    Вариант 3: Отдельный сервис (singleton)

    Если ваш класс не зависит от жизненного цикла React-компонентов и не должен перерисовывать UI при изменении состояния, его можно оставить как отдельный модуль-синглтон. В этом случае вы просто импортируете экземпляр класса в нужные компоненты и используете его методы напрямую. Однако такой подход нарушает принцип реактивности: изменения в сервисе не будут автоматически вызывать перерендер компонентов. Чтобы это исправить, придётся вручную подписываться на события или использовать библиотеки управления состоянием (например, MobX или Zustand).

    Рекомендации для миграции

    • Оцените зависимость от DOM: если класс напрямую манипулирует элементами (например, через document.getElementById), замените эти операции на React-рефы (useRef) или управляйте через состояние.
    • Разделите ответственность: методы работы с историей, хранение данных и DOM-манипуляции лучше вынести в разные хуки или модули.
    • Используйте Context API: если данные из класса нужны многим компонентам, создайте провайдер контекста, который будет передавать значения, полученные из хука.

    В итоге, для большинства случаев оптимальным решением будет создание пользовательского хука. Это сохраняет реактивность, упрощает тестирование и соответствует современным практикам React-разработки. Если же класс реализует чисто инфраструктурную логику (например, логирование или аналитику), его можно оставить как отдельный сервис, но с осторожностью.

    Часто задаваемые вопросы