Сохранение состояния Redux с помощью Redux Persist
TL;DR
Redux Persist позволяет автоматически сохранять и восстанавливать состояние Redux в хранилище (например, localStorage). В нескольких строчках кода вы оборачиваете корневой редьюсер через persistReducer и запускаете persistStore — и состояние будет сохраняться между перезапусками приложения. В статье — пошаговые примеры, подходы к миграциям, трансформеры, сценарии отката и рекомендации по безопасности и соответствию требованиям конфиденциальности.

Быстрые ссылки
- [Создание хранилища]
- [Добавление Redux Persist]
- [Конфигурация Redux Persist]
- [Объект persistor]
- [Реконcилиация состояния]
- [Миграция состояния]
- [Применение трансформаций]
- [Безопасность и конфиденциальность]
- [Альтернативы и когда не стоит использовать]
- [Тестирование и критерии приёмки]
- [Плейбук миграции и отката]
- [Короткая памятка для разных ролей]
- [Резюме]
Введение
Redux упрощает управление состоянием в сложных приложениях. Поскольку Redux-хранилище содержит всю локальную модель состояния приложения, возможность его сохранять (persist) даёт шанс восстановить сессию пользователя, настройки и другие важные данные между перезагрузками страницы или перезапусками приложения.
Определение: Redux Persist — библиотека для автоматического сохранения и восстановления состояния Redux в выбранный бэкенд-хранилище (например, localStorage в браузере). Это асинхронный слой над вашим редьюсером, который управляет сериализацией, десериализацией, миграциями и трансформациями.
Важно: сохранение состояния не заменяет серверный бэкап или авторизацию. Это локальное кэширование/псевдо-постоянное хранилище на устройстве клиента.
Создание хранилища
Предположим, вы знакомы с основами Redux. Для примера используем минимальный store и наивный редьюсер.
import {createStore} from "redux";const state = {authenticated: false};
const reducer = (state, action) => ({…state, …action});
const store = createStore(reducer, state);
Этот простой пример показывает магазин, который отслеживает, вошёл ли пользователь в систему. В текущем виде состояние создаётся заново при загрузке приложения, поэтому при перезагрузке пользователь теряет аутентификацию.
Добавление Redux Persist
Redux Persist автоматически сохраняет состояние при каждом обновлении. Вам не нужно добавлять код сохранения в действия или редьюсеры — библиотека подменяет редьюсер и обрабатывает сериализацию и запись.
Установка через npm:
npm install redux-persistПодключение к store: оборачиваем корневой редьюсер функцией persistReducer и запускаем persistStore.
import {createStore} from "redux";import {persistStore, persistReducer} from “redux-persist”;
import storage from “redux-persist/lib/storage”;
const state = {authenticated: false};
const reducer = (state, action) => ({…state, …action});
const persistConfig = { key: “root”, storage };
const persistedReducer = persistReducer(persistConfig, reducer);
const store = createStore(persistedReducer, state);
const persistor = persistStore(store);
Пояснения:
- persistReducer оборачивает ваш редьюсер, добавляя логику сохранения/восстановления.
- persistStore запускает процесс гидратации — чтение из storage и применение в store.
- storage — адаптер для выбранного механизма хранения (в примере — localStorage для веба).
## Конфигурация Redux Persist
Функция persistReducer принимает объект конфигурации. Минимально нужны ключи key и storage.
- key — имя верхнего уровня в объекте, где будет храниться сериализованное состояние (например, "root").
- storage — движок хранения (localStorage, sessionStorage, AsyncStorage для React Native, кастомный адаптер и т. п.).
Поддерживаемые backends: для веба — localStorage и sessionStorage; для React Native — AsyncStorage; возможны решения для Electron, Node.js, IndexedDB и т. д.
Создание кастомного адаптера: объект с методами setItem(), getItem(), removeItem() — каждый должен возвращать Promise, так как Redux Persist работает асинхронно.
Пример каркаса кастомного адаптера:
const myStorage = { getItem: (key) => Promise.resolve(/ строка или null /), setItem: (key, value) => Promise.resolve(), removeItem: (key) => Promise.resolve() };
Затем передаёте myStorage в persistConfig.
## Объект persistor
persistStore возвращает объект persistor с набором утилит:
- persistor.pause() — приостановить запись в хранилище.
- persistor.resume() — возобновить запись.
- persistor.flush() — заставить немедленно записать все накопленные изменения в storage.
- persistor.purge() — удалить все сохранённые данные (используйте осторожно).
Когда использовать flush(): если нужно гарантировать сохранение перед закрытием вкладки или перед выполнением критической операции.
Когда НЕ использовать purge(): вместо очистки физического хранилища предпочтительнее диспатчить Redux-экшен, который очистит состояние, и это изменение автоматически сохранятся в storage.
## Реконcилиация состояния
При гидратации Redux Persist применяет стратегию реконсилиации — как именно объединять сохранённое состояние и текущее начальное состояние приложения.
По умолчанию используется объединение на одну глубину (merge shallow): вложенные объекты не объединяются глубже одного уровня — входящее состояние перезаписывает соответствующие под-объекты в store.
Пример (по умолчанию):
- Сохранённое состояние: {"demo": {"foo": "bar"}}
- Состояние в store: {"demo": {"example": "test"}}
- Результат гидратации: {"demo": {"foo": "bar"}}
Если вам нужно более глубокое слияние, есть готовые state reconciler'ы, например autoMergeLevel2.
// импорт пропущен выше import autoMergeLevel2 from “redux-persist/lib/stateReconciler/autoMergeLevel2”;
const persistConfig = { key: “root”, storage, stateReconciler: autoMergeLevel2 };
Результат autoMergeLevel2 для примера выше:
- Сохранённое состояние: {"demo": {"foo": "bar"}}
- Состояние в store: {"demo": {"example": "test"}}
- Результат гидратации: {"demo": {"foo": "bar", "example": "test"}}
Если вы хотите полностью заменить состояние содержимым storage, используйте reconciler hardSet. Но учтите: полная замена усложняет миграции и добавление новых инициализационных свойств.
## Миграция состояния
Когда вы меняете структуру состояния между релизами, пользователи с сохранённым старым состоянием могут получить некорректное поведение. Redux Persist поддерживает миграции.
Ключи конфигурации: version и migrate.
- version — число версии состояния, вы увеличиваете его при несовместимых изменениях.
- migrate — функция или результат createMigrate, применяемый для перехода со старых версий.
Простой способ — передать функцию migrate:
const persistConfig = { key: “root”, storage, version: 1, migrate: (state) => ({…state, oldProp: undefined, newProp: “foobar”}) };
Альтернатива — объект миграций по версиям и createMigrate:
import {createMigrate} from “redux-persist”;
const migrations = { 1: state => ({…state, extraProp: true}), 2: state => ({…state, extraProp: undefined, extraPropNew: true}) };
const persistConfig = { key: “root”, storage, version: 2, migrate: createMigrate(migrations) };
Правила выполнения миграций:
- Если на устройстве версия 0 и вы ставите version: 2, будут выполнены миграции 1 и 2 последовательно.
- Если версия 1, выполнится только миграция 2.
Минимальная методология миграции:
1. Пропишите миграции в виде функций, каждая отвечает за конкретный шаг.
2. Напишите unit-тесты для каждой миграции (входное состояние -> ожидаемое выходное).
3. Обновляйте версию только при несовместимых изменениях.
4. Используйте feature- или фрагментарные миграции вместо одной большой функции, когда возможно.
## Применение трансформаций
Трансформеры (transforms) позволяют изменять данные во время записи и чтения: шифрование, сжатие, удаление чувствительных полей, смена формата и т. п.
Трансформеры — массив в конфиге, выполняются в порядке следования.
const persistStore = { key: “root”, storage, transforms: [MyTransformer] };
Пример собственного трансформера:
import {createTransform} from “redux-persist”;
const MyTransformer = createTransform( (inboundState, key) => ({…inboundState, b64: btoa(inboundState.b64)}), (outboundState, key) => ({…outboundState, b64: atob(outboundState.b64)}), {} );
Пояснение: inboundState — состояние перед сохранением (входящее), outboundState — состояние при восстановлении (исходящее). Конфигурация трансформера может содержать whitelist/blacklist имён редьюсеров для таргетинга.
Рекомендации по трансформерам:
- Шифруйте чувствительные данные (токены, PII). Не храните нешифрованные секреты в localStorage.
- Компрессия полезна для больших объёмов данных, но учитывайте CPU и задержки сериализации.
- Тестируйте производительность на слабых устройствах.
## Безопасность и конфиденциальность
LocalStorage и sessionStorage легко доступны из JavaScript, поэтому критически важно не хранить в них конфиденциальные данные без шифрования. Рассмотрите следующие меры:
- Не храните необработанные токены доступа в localStorage. Лучше — httpOnly cookies или безопасный хранилище на сервере.
- При необходимости хранения чувствительных данных — используйте криптографию на клиенте (Web Crypto API) и трансформер для шифрования/дешифрования.
- Минимизируйте объём хранящихся данных — храните только то, что нужно для UX.
- Очистка: при разлогине удаляйте чувствительные поля и/или вызывайте persistor.purge() только в экстренных случаях.
Соответствие GDPR и требования конфиденциальности:
- Проинформируйте пользователей (политика конфиденциальности), какие данные сохраняются локально.
- Дайте возможность пользователю удалить локальные данные (кнопка "Сброс данных"), которая вызывает соответствующий Redux-экшен и/или persistor.purge().
- Для данных с персональной информацией предпочтите серверные хранилища с контролем доступа.
## Альтернативы и когда не стоит использовать
Когда Redux Persist подходит:
- Нужна простая клиентская персистентность (настройки, UI-state, кеш запросов).
- Не планируется хранить чувствительные секреты.
Когда НЕ стоит использовать:
- Если требуется высокий уровень безопасности для токенов авторизации — используйте httpOnly cookies или серверное хранилище.
- Если объёмы состояния большие (несколько мегабайт), browser storage может стать узким местом.
- Если вы уже используете другую кеширующую систему (например, Apollo/RTK Query с built-in кешем и persistence), возможно, лучше использовать их инструменты.
Альтернативные подходы:
- Ручной write-thru в localStorage/IndexedDB из middleware.
- Синхронизация состояния на сервере (при логине/логоутах) — хранить минимальные данные локально и восстановление c сервера.
- Использовать специализированные хранилища (IndexedDB) для больших объёмов данных.
Сравнение практик (качественно):
- Redux Persist: быстрый старт, много готовых адаптеров, миграции и трансформеры.
- Ручной middleware: гибкость и явный контроль, но больше кода и риск ошибок.
- Серверная синхронизация: лучше для авторизованных данных и резервного копирования, но требует API и сетевого кода.
## Тестирование и критерии приёмки
Критерии приёмки для внедрения Redux Persist:
1. При запуске приложения с пустым storage — приложение инициализируется дефолтным состоянием.
2. После изменения состояния и перезагрузки — приложение восстанавливает сохранённое состояние.
3. Трансформеры корректно шифруют/дешифруют (unit-тесты для трансформеров).
4. Миграции корректно преобразуют старые версии состояния (unit-тесты для каждой миграции).
5. persistor.flush() гарантирует запись до момента следующего шага в тесте.
6. При purge() — все сохранённые данные удаляются.
Примеры тест-кейсов:
- Юзер ставит authenticated: true, перезагружает страницу — должен оставаться в системе.
- Для state с дополнительными полями, при использовании autoMergeLevel2 — поля из store и persisted объединяются.
- Миграция: входная версия 0 -> ожидаемая структура после применения миграций.
## Плейбук миграции и отката
Шаги для безопасного развёртывания миграций:
1. Локально протестируйте миграции на копиях реального состояния.
2. Напишите unit-тесты для каждой функции миграции.
3. Версионируйте релиз: увеличьте persistConfig.version и добавьте migrate.
4. Выпустите релиз с логированием (необязательно) для аналитики успешных миграций.
5. Подготовьте план отката: если миграция ломает критические фичи, выпустите хотфикс с обработчиком, который вернёт состояние в работоспособное состояние (например, очистит проблемные ветки или применит обратную миграцию).
Пример отката (runbook):
- Симптом: приложение не загружается после гидратации.
- Шаг 1: Включить журнал ошибок и собрать stack traces.
- Шаг 2: Выпустить версию с временным пропуском миграции (например, вернуть старую версию в persistConfig или добавить защитную проверку в migrate).
- Шаг 3: Если быстрое исправление невозможно — отправить инструкцию пользователям очистить локальные данные (через UI или remote push) и включить feature-flag для временной деактивации persistence.
Важно: всегда сохраняйте обратную совместимость там, где это возможно.
## Памятки для ролей
Разработчик:
- Добавьте unit-тесты для миграций и трансформеров.
- Минимизируйте хранимые данные.
- Не храните секреты без шифрования.
QA-инженер:
- Тестируйте гидратацию на реальных устройствах и в разных браузерах.
- Проверьте сценарии обновления версии и отката.
Product Manager:
- Определите, какие данные критично сохранять локально.
- Согласуйте политику удаления и срок хранения данных.
DevOps:
- Убедитесь, что release notes содержат информацию об изменениях persistConfig.version.
- Подготовьте мониторинг ошибок, связанных с гидратацией и миграциями.
## Шпаргалка и шаблоны
Шаблон минимальной конфигурации:
import {persistStore, persistReducer} from “redux-persist”; import storage from “redux-persist/lib/storage”;
const persistConfig = { key: “root”, storage, version: 1, migrate: (state) => state };
const persistedReducer = persistReducer(persistConfig, rootReducer); const store = createStore(persistedReducer, initialState); const persistor = persistStore(store);
Шаблон миграции:
const migrations = { 1: (state) => {
// добавить новое поле с безопасным значением
return {...state, featureFlagX: false};}, 2: (state) => {
// удалить устаревшее поле
const {oldProp, ...rest} = state;
return {...rest, newProp: transformOld(oldProp)};} };
Чек-лист производительности:
- Оцените размер сериализованного состояния.
- Ограничьте частоту обновлений, если запись в storage выходит боком (debounce/throttle).
- Используйте selective persisting: whitelist/blacklist редьюсеров для уменьшения данных.
## Совместимость и миграция между платформами
- Web: localStorage/sessionStorage (synchronous APIs), IndexedDB через адаптеры для больших данных.
- React Native: AsyncStorage (не путать с веб-AsyncStorage).
- Electron/Node: требуется кастомный адаптер (файловая система или LevelDB).
Советы при кросс-платформенной миграции:
- Форматируйте данные в переносимый JSON-формат.
- Учитывайте ограничения storage в каждой среде (quota, синхронность).
- Тестируйте гидратацию при переносе состояния между платформами.
## Примеры ошибок и способы их диагностики
Типичные проблемы:
- Ошибка парсинга JSON: corrupted data в storage — добавьте try/catch и fallback на дефолтное состояние.
- Произвольные undefined-поля после миграции — допишите проверки и дефолтные значения в редьюсерах.
- Большая задержка при старте из-за тяжёлой десериализации — подумайте о lazy-hydration или отображении skeleton UI до завершения гидратации.
Диагностика:
- Логи в консоль при ошибках гидратации с детальной информацией о версии и ключах.
- Возможность принудительной очистки persisted state из UI для восстановления работоспособности.
## Галерея крайних случаев
- Очень большие объёмы стейта: лучше хранить часть данных в IndexedDB или на сервере.
- Частая запись большого состояния: используйте selective persist + throttle.
- Конфликты форматов между релизами: явно версионируйте структуры и пишите обратные миграции.
## Краткий глоссарий
- persistor — объект управления персистентностью (pause/resume/flush/purge).
- persistReducer — функция-обёртка над редьюсером для добавления persistence-логики.
- transform — функция, изменяющая данные при записи/чтении.
- migrate/version — механизмы управления эволюцией сохранённой структуры.
## Резюме
Redux Persist — надёжный инструмент для добавления локальной персистентности в Redux-приложения. Он упрощает сохранение состояния, предоставляет миграции и трансформации и поддерживает разные backend'ы хранения. Перед внедрением продумайте безопасность данных, размер состояния и стратегию миграций. Тестируйте миграции и трансформеры, и держите план отката под рукой.
Важно
- Никогда не храните необработанные секреты в localStorage без шифрования.
- Тестируйте миграции и планируйте версионирование состояния.
## Короткое резюме для соцсетей
Redux Persist позволяет сохранять состояние приложения между сессиями — быстро включается, поддерживает миграции и трансформеры. Подходит для UI-state и настроек, но требуются меры безопасности для СНИП/PII.Похожие материалы
Несколько аккаунтов Skype: Multi Skype Launcher
Журнал для работы: повысить продуктивность
Персональные звуки уведомлений на Android
Скачивание шоу Hulu для офлайн‑просмотра
Microsoft Start: персонализированная новостная лента