State Management

Persistence и гидрация

Пользователь настроил тёмную тему, свернул половину панелей и собрал черновик заявки. Перезагрузка страницы - и всё сбросилось к начальному состоянию. Решение - сохранять часть состояния в localStorage и поднимать его обратно при старте. Но тут появляются две ловушки. Первая: при SSR сервер рендерит светлую тему, а клиент после гидрации видит сохранённую тёмную, и React ругается на расхождение. Вторая: через месяц форма черновика изменилась, а в localStorage лежит старая. Персистентность это не просто запись в хранилище.

  • Тема, язык и свёрнутые панели, переживающие перезагрузку через localStorage
  • Черновик длинной формы или заявки, сохраняемый локально, чтобы не потерять ввод при закрытии вкладки
  • Офлайн-кэш или большие наборы данных в IndexedDB, куда не помещается localStorage
  • SSR-приложения, где сохранённое состояние гидрируется без расхождения между сервером и клиентом
  • Команды, добавившие версию схемы и миграции, чтобы старое сохранённое состояние не ломало новый код

Предварительные знания

  • Неизменяемость состояния и почему обновление создаёт новую структуру, а не мутирует старую
  • Нормализованная форма состояния: данные по идентификаторам, а не вложенными копиями
  • Идея сериализации: превращение состояния в строку и обратно
  • Неизменяемость и нормализация

Куда сохранять: localStorage и IndexedDB

Персистентность это сохранение части состояния в браузерное хранилище, чтобы оно пережило перезагрузку. Два основных хранилища решают разные задачи. localStorage хранит строки синхронно, прост в использовании и подходит для небольших значений вроде темы, языка и набора свёрнутых панелей. IndexedDB асинхронна, держит структурированные данные большого объёма и подходит для офлайн-кэша и крупных наборов.

Сохраняют не всё состояние, а только то, что должно пережить перезагрузку. Серверные данные из кэша запросов персистить обычно не нужно: их проще загрузить заново. Сохранять стоит чисто клиентские настройки и черновики. Чтение возвращает разобранное значение или null, и тип проверяется guard-функцией, потому что в хранилище могла остаться старая или испорченная запись.

СвойствоlocalStorageIndexedDB
ДоступСинхронныйАсинхронный
ОбъёмНесколько мегабайтСотни мегабайт и больше
ДанныеТолько строкиСтруктурированные объекты
Где уместноТема, язык, мелкие настройкиОфлайн-кэш, крупные наборы

localStorage синхронный, поэтому запись большого объёма блокирует основной поток и подмораживает интерфейс. Для крупных или часто меняющихся данных это IndexedDB. И сохранённое значение всегда проверяют при чтении: оно могло устареть или быть повреждённым, доверять ему вслепую нельзя.

Какое хранилище уместнее для большого офлайн-кэша структурированных данных?

Гидрация сохранённого состояния при SSR

При серверном рендеринге появляется тонкость. Сервер генерирует HTML, не имея доступа к localStorage браузера: на сервере его просто нет. Затем клиент получает этот HTML и гидрирует его, оживляя разметку. Если первый клиентский рендер сразу применит сохранённую тему, он разойдётся с серверным HTML, и React сообщит о расхождении гидрации (hydration mismatch).

Лечение в том, чтобы первый клиентский рендер совпадал с серверным, а сохранённое значение применять уже после монтирования. На практике состояние стартует с того же дефолта, что и на сервере, и подмена сохранённым значением происходит в эффекте после первого рендера. Тогда разметка сервера и клиента совпадает, а сохранённая тема применяется следующим шагом.

Альтернатива для темы, чтобы избежать вспышки светлого фона, это маленький инлайн-скрипт в шапке документа, который читает localStorage и ставит класс на корневой элемент до гидрации React. Но само React-состояние всё равно должно стартовать с серверного дефолта, чтобы разметка совпала.

Почему при SSR опасно сразу применять сохранённую в localStorage тему в первом клиентском рендере?

Версионирование и миграции схемы

Сохранённое состояние живёт долго, а код меняется. Через месяц форма данных другая: поле переименовали, добавили новое, изменили структуру. А в localStorage у части пользователей лежит старая версия. Если поднять её в новый код как есть, приложение получит данные не той формы и сломается. Защита от этого - версия схемы и функция миграции.

Каждая сохранённая запись несёт поле version. При чтении migrate смотрит на версию: если она старая, функция превращает данные в актуальную форму, например выводит новое поле theme из прежнего darkMode. Если версия уже актуальная и форма подтверждена guard-функцией, запись принимается. Если данные неузнаваемы, возвращается null, и приложение стартует с дефолта.

  1. Каждая запись персистентного состояния несёт номер версии схемы
  2. При чтении версия записи сравнивается с текущей версией кода
  3. Старая версия проводится через цепочку миграций до актуальной формы
  4. Результат проверяется guard-функцией, чтобы гарантировать ожидаемую структуру
  5. Неузнаваемые или повреждённые данные отбрасываются, приложение берёт дефолт

Версионирование персистентного состояния родственно миграциям схемы базы данных: и там, и там старые данные нужно безопасно привести к новой форме. Библиотеки персистентности вроде persist-middleware для Zustand встраивают поле version и колбэк миграции прямо в настройку стора.

Зачем хранить номер версии схемы вместе с персистентным состоянием?

Связь с другими темами

Этот урок про сохранение состояния. Рядом стоит его форма и устройство:

  • Неизменяемость и нормализация — Сохраняется и мигрируется именно нормализованная неизменяемая форма, поэтому её устройство важно для персистентности

Итог

  • Персистентность сохраняет часть состояния в хранилище браузера и поднимает его обратно при старте приложения
  • localStorage подходит для небольшого синхронного состояния, IndexedDB для крупных и структурированных данных
  • Сохраняют не всё состояние, а лишь то, что должно пережить перезагрузку: тему, черновик, свёрнутые панели
  • При SSR сервер не видит localStorage, поэтому первый рендер и гидрация должны совпадать, иначе React сообщает о расхождении
  • Расхождение лечится тем, что сохранённое состояние применяют после монтирования, а не в первый серверный рендер
  • Версия схемы и функция миграции защищают код от старого сохранённого состояния при изменении его формы

Связанные уроки

  • sm-05-immutability-normalization — Нормализованная и неизменяемая форма состояния это то, что сохраняется и мигрируется, поэтому её устройство важно для персистентности
Persistence и гидрация

0

1

Войти