Ошибки гидратации в React и способы их устранения

22 сентября 2026
Автор

Антон Ларичев

Что такое гидратация в React

Гидратация (hydration) — это процесс, при котором React берёт HTML, уже отрендеренный на сервере, и «оживляет» его на клиенте: подключает обработчики событий, инициализирует состояние и синхронизирует виртуальный DOM с реальным.

При серверном рендеринге (SSR) последовательность выглядит так:

  1. Сервер выполняет React-компоненты и отдаёт браузеру готовый HTML.
  2. Браузер показывает этот HTML пользователю — страница уже видна, но не интерактивна.
  3. Загружается JavaScript-бандл.
  4. React запускается на клиенте и проходит по тому же дереву компонентов.
  5. React сравнивает виртуальный DOM с реальным DOM — это и есть гидратация.
  6. Если они совпадают, React только навешивает обработчики, не перерисовывая DOM.

Если на шаге 5 структура или содержимое не совпадают, React выбрасывает ошибку гидратации и в production заменяет серверный HTML клиентским рендером, что приводит к вспышке перерисовки (flash of content).

Курс по теме

Курс React и Redux Toolkit с нуля

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Как выглядит ошибка гидратации

В консоли браузера ошибка гидратации выглядит примерно так:

Warning: Text content did not match.
  Server: "Привет, Антон"
  Client: "Привет, Гость"

Или в React 18+:

Error: Hydration failed because the initial UI does not match what was rendered on the server.

В React 18 ошибки гидратации стали жёстче: вместо предупреждения они бросают исключение, которое может привести к размонтированию всего дерева, если не обработано через Error Boundary.

Частые причины ошибок гидратации

Использование Date и Math.random()

Самый распространённый сценарий — вывод текущей даты или случайного значения прямо в JSX.

// Плохо: сервер и клиент выполняются в разное время
function Timestamp() {
  return <span>{new Date().toLocaleTimeString()}</span>;
}

Сервер рендерит «12:00:01», а клиент в момент гидратации выдаёт «12:00:03» — расхождение гарантировано.

То же самое с Math.random():

// Плохо: каждый вызов даёт новое значение
function Avatar() {
  const seed = Math.floor(Math.random() * 1000);
  return <img src={`https://picsum.photos/seed/${seed}/50/50`} />;
}

Чтение window, localStorage, navigator

Объект window на сервере не существует. Если обратиться к нему при рендере, код просто упадёт с ReferenceError. Но даже защита через typeof window !== 'undefined' создаёт расхождение:

// Плохо: сервер рендерит один вариант, клиент — другой
function Theme() {
  const theme = typeof window !== 'undefined'
    ? localStorage.getItem('theme') ?? 'light'
    : 'light';

  return <div className={`app app--${theme}`}>...</div>;
}

Сервер всегда выдаст app--light, а клиент может выдать app--dark — расхождение в атрибуте className.

Условный рендеринг по User-Agent

// Плохо: сервер и клиент могут видеть разный UA
function MobileMenu() {
  const isMobile = navigator.userAgent.includes('Mobile');
  if (!isMobile) return null;
  return <nav className="mobile-menu">...</nav>;
}

Даже если сервер пробрасывает заголовок User-Agent, после гидратации браузер вычисляет его заново и может получить иное значение.

Неправильно закрытые HTML-теги

Если в JSX или шаблоне нарушена вложенность тегов, браузер самостоятельно исправляет HTML — и получившееся дерево отличается от того, что построил React.

// Плохо: таблица без tbody
function Table({ rows }) {
  return (
    <table>
      {rows.map(row => (
        <tr key={row.id}>
          <td>{row.name}</td>
        </tr>
      ))}
    </table>
  );
}

Браузер вставляет <tbody> автоматически — React видит <tbody> там, где его нет в виртуальном DOM.

Сторонние библиотеки и порталы

Некоторые UI-библиотеки добавляют элементы в document.body или используют window при инициализации. Такие компоненты изначально несовместимы с SSR без специальной обработки.

Способы устранения ошибок гидратации

1. Перенести динамический контент в useEffect

Золотое правило: всё, что зависит от браузерного окружения или меняется между сервером и клиентом, должно вычисляться в useEffect — он выполняется только на клиенте, после гидратации.

import { useState, useEffect } from 'react';

function Timestamp() {
  const [time, setTime] = useState<string | null>(null);

  useEffect(() => {
    setTime(new Date().toLocaleTimeString());
    const id = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);
    return () => clearInterval(id);
  }, []);

  if (time === null) return <span>--:--:--</span>;
  return <span>{time}</span>;
}

Сервер и клиент при первом рендере оба выдадут «--:--:--». После гидратации useEffect запустится и установит реальное время.

2. Паттерн isMounted

Универсальный паттерн для компонентов, которые не должны рендерить браузерозависимый контент до завершения гидратации:

import { useState, useEffect } from 'react';

function useIsMounted() {
  const [isMounted, setIsMounted] = useState(false);

  useEffect(() => {
    setIsMounted(true);
  }, []);

  return isMounted;
}

function ThemeToggle() {
  const isMounted = useIsMounted();
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    const saved = localStorage.getItem('theme') ?? 'light';
    setTheme(saved);
  }, []);

  if (!isMounted) {
    return <button disabled>Загрузка...</button>;
  }

  return (
    <button onClick={() => {
      const next = theme === 'light' ? 'dark' : 'light';
      setTheme(next);
      localStorage.setItem('theme', next);
    }}>
      {theme === 'light' ? 'Тёмная тема' : 'Светлая тема'}
    </button>
  );
}

3. Компонент ClientOnly

Если таких мест в приложении много, удобно вынести логику в переиспользуемый компонент-обёртку:

import { useState, useEffect, ReactNode } from 'react';

interface ClientOnlyProps {
  children: ReactNode;
  fallback?: ReactNode;
}

function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
  const [hasMounted, setHasMounted] = useState(false);

  useEffect(() => {
    setHasMounted(true);
  }, []);

  if (!hasMounted) return <>{fallback}</>;
  return <>{children}</>;
}

// Использование
function App() {
  return (
    <div>
      <h1>Мой сайт</h1>
      <ClientOnly fallback={<p>Загрузка карты...</p>}>
        <MapComponent />
      </ClientOnly>
    </div>
  );
}

4. suppressHydrationWarning

Для случаев, когда расхождение неизбежно и некритично (например, временная метка, которую вы немедленно обновите через useEffect), React предоставляет атрибут suppressHydrationWarning:

function LastUpdated() {
  const [time, setTime] = useState(new Date().toLocaleTimeString());

  useEffect(() => {
    setTime(new Date().toLocaleTimeString());
  }, []);

  return (
    <time suppressHydrationWarning>
      {time}
    </time>
  );
}

Атрибут работает только на уровне одного элемента и его прямых текстовых потомков. Он не подавляет ошибки в дочерних компонентах и не является заменой правильной архитектуры — используйте его только как последнее средство.

5. Директива use client в Next.js App Router

В Next.js 13+ с App Router компоненты по умолчанию серверные. Если компонент использует браузерные API, хуки состояния или эффекты, его нужно явно пометить:

'use client';

import { useState, useEffect } from 'react';

export function UserGreeting() {
  const [name, setName] = useState<string | null>(null);

  useEffect(() => {
    setName(localStorage.getItem('username'));
  }, []);

  return <p>Привет, {name ?? 'Гость'}!</p>;
}

Директива 'use client' не означает «не рендерить на сервере» — компонент всё равно получит серверный рендер. Она означает «этот компонент будет гидратирован на клиенте». Чтобы полностью отключить SSR для компонента, используйте dynamic с ssr: false:

import dynamic from 'next/dynamic';

const HeavyClientComponent = dynamic(
  () => import('./HeavyClientComponent'),
  { ssr: false, loading: () => <p>Загрузка...</p> }
);

6. Правильная HTML-структура

Проверяйте, что вложенность тегов корректна. Особенно часто проблемы возникают с таблицами и параграфами:

// Правильно: явный tbody
function DataTable({ rows }) {
  return (
    <table>
      <thead>
        <tr>
          <th>Имя</th>
          <th>Email</th>
        </tr>
      </thead>
      <tbody>
        {rows.map(row => (
          <tr key={row.id}>
            <td>{row.name}</td>
            <td>{row.email}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// Плохо: параграф внутри параграфа (браузер сломает структуру)
function BadParagraph() {
  return (
    <p>
      Текст
      <p>Вложенный параграф</p>
    </p>
  );
}

Отладка ошибок гидратации

Читайте сообщение об ошибке внимательно

React 18 выводит конкретное расхождение: что было на сервере и что пришло с клиента. Это сразу указывает на проблемное место.

Сравните серверный HTML с клиентским

Откройте DevTools, перейдите на вкладку Elements и посмотрите на HTML сразу после загрузки страницы (до гидратации это сложнее, но можно отключить JavaScript и посмотреть «чистый» серверный HTML). Сравните с тем, что рендерит компонент на клиенте.

Используйте React DevTools

Extension React DevTools для Chrome и Firefox показывает дерево компонентов после гидратации. Если компонент помечен как имеющий несоответствие, он будет выделен.

Временно добавьте логирование

function DebugComponent() {
  const isServer = typeof window === 'undefined';
  console.log('Рендер на', isServer ? 'сервере' : 'клиенте');

  return <div>...</div>;
}

Посмотрите в логах сервера и в консоли браузера — что именно рендерится в каждом окружении.

Лучшие практики

Делайте серверный рендер детерминированным. Компоненты должны возвращать одинаковый результат при одинаковых пропсах. Если результат зависит от времени, случайных чисел или браузерного окружения — это сигнал, что логику нужно перенести в useEffect.

Разделяйте серверные и клиентские компоненты. Держите браузерозависимую логику в листовых компонентах, как можно ближе к краю дерева. Чем меньше компонент, который оборачивается в ClientOnly или помечается 'use client', тем лучше.

Не используйте suppressHydrationWarning как заглушку. Это средство для исключительных случаев, а не способ «замести под ковёр» архитектурную проблему.

Тестируйте SSR явно. Пишите тесты, которые рендерят компоненты через renderToString из react-dom/server и проверяют, что вывод совпадает с клиентским рендером:

import { renderToString } from 'react-dom/server';
import { render } from '@testing-library/react';

test('не вызывает ошибок гидратации', () => {
  const serverHtml = renderToString(<MyComponent />);
  const container = document.createElement('div');
  container.innerHTML = serverHtml;

  expect(() => {
    render(<MyComponent />, { container, hydrate: true });
  }).not.toThrow();
});

Синхронизируйте начальное состояние через пропсы, а не через браузерные API. Если странице нужны данные пользователя при первом рендере, пробрасывайте их как пропсы с сервера, а не читайте из localStorage при монтировании:

// Серверный компонент (Next.js)
async function Page() {
  const user = await getUserFromSession();
  return <UserDashboard initialUser={user} />;
}

// Клиентский компонент
'use client';
function UserDashboard({ initialUser }) {
  const [user, setUser] = useState(initialUser);
  // Сервер и клиент стартуют с одинаковым значением
  return <div>Привет, {user.name}!</div>;
}

Ошибки гидратации — это сигнал о рассинхронизации между двумя средами выполнения. Понимание этого разрыва и осознанное управление тем, где происходит каждый тип вычислений, позволяет строить надёжные SSR-приложения без мерцания и неожиданных перерисовок.

Чтобы глубже разобраться в React, включая серверный рендеринг, хуки и паттерны проектирования компонентов, приходите на курс: React — полный курс для разработчиков

Стрелочка влевоЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

React тестирование с Vitest
React с TanStack Router
Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиReact с WebSocket — реалтайм в компонентахReact Native приложение с нуля до публикации в сторахЧастичное применение: как создавать компоненты без лишнего кодаПрофилирование React: как найти и устранить узкие местаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыОшибки гидратации в React и способы их устраненияHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDebugValue — отладка кастомных хуковuseDeferredValue — отложенное обновление состоянияuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияХук use() в React 19useEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextНовые возможности React 19Как предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияflushSync — синхронные обновления состояния в ReactuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьПорталы в React: рендер компонентов вне иерархии DOMРабота с children в ReactFragment в React: группировка элементов без лишних узлов DOMConcurrent Mode — конкурентный режим в ReactCSS Modules в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий