Синхронизация Redux store между вкладками с помощью redux-state-sync

Быстрые ссылки
- Обзор
- Синхронизация хранилища
- Настройка синхронизации
- Интеграция с Redux-Persist
- Критерии приёмки
- Чек-листы ролей
- Тесты и сценарии
- Когда это не подходит
- Итог
Обзор
redux-state-sync — это библиотека, предоставляющая middleware для Redux, которое пересылает диспатченные действия между вкладками одного origin. Каждый набор вкладок подключается к каналу (Broadcast Channel или запасной механизм), и любые действия, прошедшие через middleware, транслируются другим вкладкам, где они тоже диспатчатся в локальный store.
Ключевые идеи в одной строке:
- Middleware перехватывает действие и отправляет его в межвкладочный канал.
- Каждая вкладка запускает слушатель сообщений и, получив действие, диспатчит его в свой store.
- По умолчанию используется Broadcast Channel API; если он недоступен, библиотека падает на IndexedDB или LocalStorage.
Когда применять:
- Синхронизация логина/логаута, тем оформления, уведомлений, счётчиков непрочитанных сообщений.
- Удобно для одностраничных приложений, где пользователь может открыть несколько вкладок одновременно.
Ограничения:
- Передаваемые данные должны поддерживаться алгоритмом структурного клонирования браузера (structured clone). Сложные объекты (с методами, ссылками на DOM, нестандартными классами) могут быть не полностью восстановлены.
Важно: библиотека не автоматически переносит существующее состояние одной вкладки в новую вкладку — для этого есть дополнительная функция initStateWithPrevTab().
Установка и быстрая настройка
Установите пакет через npm или yarn:
npm install redux-state-syncПростой пример store до добавления синхронизации:
import {createStore} from "redux";
const state = {authenticated: false};
const reducer = (state, action) => ({...state, ...action});
const store = createStore(reducer, state);Добавим middleware и инициализируем слушатель сообщений:
import {createStore, applyMiddleware} from "redux";
import {createStateSyncMiddleware, initMessageListener} from "redux-state-sync";
const reduxStateSyncConfig = {};
const reducer = (state, action) => ({...state, ...action});
const store = createStore(
reducer,
state,
applyMiddleware(createStateSyncMiddleware(reduxStateSyncConfig))
);
initMessageListener(store);Теперь, если открыть сайт в двух вкладках и задиспатчить действие в одной, оно появится и во второй вкладке. В DevTools можно увидеть входящее действие и изменение состояния.
Если вы хотите, чтобы новая вкладка сразу инициализировала состояние из уже открытой вкладки, используйте initStateWithPrevTab():
const store = createStore(reducer, state, applyMiddleware(createStateSyncMiddleware({})));
initStateWithPrevTab(store);initStateWithPrevTab заменит локальный state новой вкладки состоянием из предыдущей вкладки, если такая найдена.
Настройка синхронизации
createStateSyncMiddleware принимает объект конфигурации с параметрами, которые позволяют тонко управлять поведением пересылки действий. Ниже — наиболее полезные опции с примерами.
Исключение действий
Если у вас есть действия, которые не должны синхронизироваться (например, открытие модального окна локально), используйте blacklist:
const config = {
blacklist: ["DEMO_ACTION"]
};
// Это действие не будет синхронизировано с другими вкладками
store.dispatch({type: "DEMO_ACTION"});Альтернативно, примените whitelist чтобы синхронизировать только явно указанные действия:
const config = {
whitelist: ["LOGIN_SUCCESS", "LOGOUT", "SET_THEME"]
};Точное фильтрование действий
Если вам нужно правило более гибкое, используйте predicate. Это функция, которая получает действие и возвращает true/false для разрешения синхронизации:
const config = {
predicate: action => action && action.type && action.meta && action.meta.sync !== false
};Пример выше синхронизирует только действия, у которых есть meta.sync !== false.
Настройки Broadcast Channel
По умолчанию используется канал с именем redux_state_sync. Вы можете изменить его и передать опции адаптера:
const config = {
channel: "my_app_channel",
broadcastChannelOption: {type: "idb"}
};Параметр broadcastChannelOption передаётся библиотеке pubkey/broadcast-channel и позволяет жестко задать используемую технологию (например, IndexedDB) даже при наличии нативного Broadcast Channel.
Интеграция с redux-persist
Если вы используете Redux Persist, не вызывайте initStateWithPrevTab — достаточно initMessageListener, потому что persisted state уже восстановится из хранилища при загрузке вкладки.
Рекомендуется исключить действия lifecycle Redux Persist из синхронизации:
const config = {
blacklist: ["persist/PERSIST", "persist/REHYDRATE"]
};Практические советы по дизайну действий
- Передавайте в действия только сериализуемые данные (plain objects, массивы, числа, строки, Blob). Избегайте функций и экземпляров классов.
- Для больших полезных нагрузок (payload) рассмотрите хранение тяжелых данных в IndexedDB и передачу по каналу только идентификатора/сигнала для загрузки.
- Явно указывайте в meta, какие действия должны быть синхронизированы, чтобы избежать случайной рассинхронизации.
Примеры конфигураций и сниппеты
Минимальная конфигурация:
import {createStateSyncMiddleware, initMessageListener} from "redux-state-sync";
const config = {};
const store = createStore(reducer, applyMiddleware(createStateSyncMiddleware(config)));
initMessageListener(store);Конфигурация с whitelist и кастомным каналом:
const config = {
whitelist: ["LOGIN_SUCCESS", "LOGOUT", "SET_PREFS"],
channel: "my_app_redux_channel"
};Конфигурация, заставляющая использовать IndexedDB через адаптер:
const config = {
broadcastChannelOption: {type: "idb"}
};Критерии приёмки
- При диспатче действия X в вкладке A действие X появляется в DevTools и в store вкладки B (если вкладка B открыта).
- При логине в одной вкладке пользователь помечается как authenticated во всех открытых вкладках.
- Исключённые в blacklist действия не распространяются в другие вкладки.
- В случае использования initStateWithPrevTab новая вкладка получает состояние от уже открытой вкладки.
- В случаях отсутствия Broadcast Channel синхронизация продолжает работать через IndexedDB/LocalStorage.
Тесты и сценарии приёмки
Функциональные тесты:
- Открыть две вкладки, выполнить LOGIN → убедиться, что оба store отмечают authenticated: true.
- Диспатчить blacklist-действие → убедиться, что оно не приходит во вторую вкладку.
- Переключить сеть (offline) → диспатч в другой вкладке; при возврате онлайн проверить состояние.
Интеграционные тесты:
- С поддержкой Redux Persist: перезагрузить вкладку и убедиться, что persisted state корректно загружается и не конфликтует с приходящими сообщениями.
Нагрузочные тесты:
- Отправить серию быстрых действий и проверить, что не возникает гонок, и что приемник корректно применяет их в ожидаемом порядке.
Рольовые чек-листы
Разработчик:
- Добавить middleware и initMessageListener.
- Определить whitelist/blacklist и predicate.
- Убедиться, что actions сериализуемы.
QA инженер:
- Подготовить тест-кейсы по критериям приёмки и авто-тесты.
- Проверить поведение при отсутствии Broadcast Channel (эмуляция в браузере/среде).
DevOps:
- Проверить, что политики безопасности CSP не блокируют механизм хранения (IndexedDB/LocalStorage).
- Убедиться, что сборки включают библиотеку и не минимизируют её так, чтобы сломать интеграцию.
Product менеджер:
- Решить, какие доменные действия надо синхронизировать и почему.
- Протоколировать ожидаемое поведение для UX (например, при логине/логауте).
Планы отката и обработка инцидента
Если после внедрения синхронизации наблюдаются эффекты рассинхронивания или некорректные состояния:
- Быстрый откат: убрать middleware из продовой сборки и развёрнуть фикс.
- Диагностика: включить логирование действий, приходящих через канал, и сравнить с локальными диспатчами.
- Временное решение: применить whitelist, чтобы синхронизировать только безопасные действия, пока не найдена причина.
Когда это не подходит
- Если ваше приложение опирается на несерилизуемые объекты или сложные экземпляры классов в action.payload.
- Если требуется строгая атомарная синхронизация большого и сложного состояния (в этом случае лучше реализовать централизованное серверное решение или использовать WebSocket синхронизацию с сервером).
- Если поведение приложения подразумевает, что некоторые действия должны оставаться строго локальными и не быть видимыми другим вкладкам (например, временные UI-состояния без смысла для других вкладок).
Совместимость и тонкие моменты
- Broadcast Channel поддерживается в современных браузерах; старые версии могут полагаться на IndexedDB или LocalStorage. Всегда тестируйте в целевых браузерах.
- Structured clone не передаёт функции и прототипы классов. При необходимости используйте сериализацию в payload (JSON) и явное восстановление на стороне получателя.
- При использовании SSR (server-side rendering) убедитесь, что никто из серверных процессов не пытается инициализировать слушатель межвкладочного канала.
Риски и смягчения
Риск: рассинхронизация из-за нестабильного порядка сообщений или долгой доставки. Митигирование: добавить в payload метку времени и версию состояния; на получателе применять логику idempotency и проверять версию.
Риск: утечка приватных данных через межвкладочный канал. Митигирование: никогда не включать в action личные токены или чувствительные поля; передавать только сигналы, а при необходимости — краткие индикаторы.
Мини-методология внедрения
- Проанализировать доменные действия и выбрать whitelist/blacklist.
- Внедрить middleware в dev-ветке и покрыть автоматизированными тестами.
- Прогнать QA-скрипты в целевых браузерах и на мобильных устройствах.
- Постепенно выкатывать в production и мониторить логи.
Шпаргалка конфигурации (cheat sheet)
- По умолчанию: channel = “redux_state_sync”; broadcastChannelOption = undefined
- Принудительный IndexedDB: broadcastChannelOption: {type: “idb”}
- Исключение Persist действий: blacklist: [“persist/PERSIST”,”persist/REHYDRATE”]
- Синхронизация только важных действий: whitelist: [“LOGIN_SUCCESS”,”LOGOUT”,”SET_THEME”]
Примеры тестов/приёмки (специфичные кейсы)
- Тест: логин в вкладке A → вкладка B получает действие LOGIN_SUCCESS → state.authenticated === true.
- Тест: диспатч модального открытия DEMO_ACTION в вкладке A → вкладка B не получает DEMO_ACTION (при blacklist).
- Тест: открыть вкладку C после вкладки A и вызвать initStateWithPrevTab → C получает состояние A.
Безопасность и приватность
- Не отправляйте через межвкладочный канал секреты: JWT, refresh токены или персональные данные. Эти данные должны храниться в защищённых хранилищах и не пересылаться между вкладками.
- GDPR/приватность: если вы передаёте персональные идентификаторы, убедитесь, что это соответствует политике обработки данных. По возможности передавайте только неизбыточные индикаторы.
Миграция и совместимость версий
- При обновлении redux-state-sync проверяйте релиз-ноты на изменения API createStateSyncMiddleware и initMessageListener.
- Тестируйте поведение в режиме с несколькими версионируемыми вкладками — если старые вкладки остаются с предыдущей версией, может возникнуть несовместимость формата сообщений.
Мероприятие принятия решения (диаграмма)
flowchart TD
A[Нужно ли синхронизировать действие?] -->|Нет| B[Оставить локальным]
A -->|Да| C{Данные сериализуемы?}
C -->|Нет| D[Использовать серверную синхронизацию или IndexedDB]
C -->|Да| E{Нужно ли сразу передавать полное состояние новой вкладке?}
E -->|Да| F[Использовать initStateWithPrevTab]
E -->|Нет| G[Передавать отдельные действия через redux-state-sync]1-строчный глоссарий
- Broadcast Channel: браузерный API для обмена сообщениями между вкладками одного origin.
- initMessageListener: функция, регистрирующая слушатель межвкладочных сообщений для вашего store.
- createStateSyncMiddleware: middleware, которое транспонирует действия в межвкладочный канал.
Итог
redux-state-sync — это недорогой по интеграции инструмент для решения типичных UX-проблем, когда у пользователя открыто несколько вкладок сайта. Он позволяет синхронизировать полезные действия (логин, логаут, изменения настроек) без сложной инфраструктуры. Важно правильно выбрать набор синхронизируемых действий, убедиться в сериализуемости payload и протестировать поведение в целевых браузерах.
Короткие рекомендации:
- Начните с whitelist для ключевых событий.
- Исключите lifecycle-действия Redux Persist.
- Не отправляйте чувствительные данные через канал; при необходимости передавайте только идентификаторы и сигналы.
Полезная деталь: минифицированная библиотека redux-state-sync занимает ~19 КБ, и для базовой синхронизации достаточно нескольких строк кода.
Похожие материалы
Запуск DOS‑игры на iPad через iDOS 3
Ошибка: принтер используется другим компьютером — решение
Искривление текста в Photoshop и Illustrator
Payday 2 VR не работает — как исправить
Эмулятор HoloLens — запуск и управление приложениями