Связываем React и localStorage через useSyncExternalStore

  —  4 минуты

#web#javascript#theory#data#code#react
Читать статью в Telegram

Как согласовать изменение состояния в реакте и поля в localStorage?

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

Также можно обработать какое-то не-реактовое значение через комбинацию useState + useEffect, но это ещё менее надёжно, ведь браузерные значения могут меняться и без уведомления реакта, и, соответственно, без ререндера

Красиво в одной из статей на хабре описали:

Для работы с состоянием в React используются хуки useState и useReducer, но они не умеют работать с состоянием, которое "живет" за пределами React, поскольку в один момент времени доступна только одна версия внешнего состояния.

Значения внешнего состояния могут меняться со временем без ведома React, что может приводить к таким проблемам, как отображение двух разных значений для одних и тех же данных.

Но не так давно в 18 версию React добавили хук useSyncExternalStore, который такую задачу решает намного изящнее

Многие скипнули его и даже не знают зачем он нужен, что, в целом, достаточно ожидаемо, ведь даже команда разработчиков позиционировала его больше как хук для разработчиков библиотек, а мы тут далеко не все пишем свои либы

Короче, что это за хук вообще? Очень просто — этот хук нужен для более глубокой интеграции внешних хранилищ в модель React. Говоря проще — хук нужен для того, чтобы триггерить рендер из внешних хранилищ, а не только через setState функции

Как раз этот хук и поможет нам интегрироваться с localStorage сильно проще и безопаснее. Тут localStorage в понятие внешнего хранилища ложится просто шикарно

На коленке код будет выглядеть примерно так:

typescript
1const useLocalStorageState = (key: string, defaultValue?: string) => {
2    const subscribe = (listener: () => void) => {
3        window.addEventListener("update-local-storage", listener);
4        return () => void window.removeEventListener("update-local-storage", listener);
5    };
6    
7    const getSnapshot = () => localStorage.getItem(key) ?? defaultValue;
8    
9    const store = useSyncExternalStore(subscribe, getSnapshot);
10    
11    const updateStore = (newValue: string) => {
12        localStorage.setItem(key, newValue);
13        window.dispatchEvent(new StorageEvent("update-local-storage", { key, newValue }));
14    };
15    
16    return [store, updateStore] as const;
17};
18

В чём тут идея:

  1. При вызове updateStore будем помимо изменения значения в localStorage диспатчить на window ещё и StorageEvent с ключом, например, "update-local-storage"
  2. В функции подписки subscribe объясним когда нужно вызывать getSnapshot для получения актуального состояния из внешнего хранилища и когда от его прослушивания нужно отписаться. Можно воспринимать как эффект

Использовать будем как обычный useState:

typescript
1const [name, setName] = useLocalStorageState("name", "progway");
2

Теперь хук при вызове с одним и тем же ключом к localStorage (name в примере выше) будет обновлять все зависимые компоненты при регистрации события "update-local-storage" на window

Используя тот же подход, можно реализовать порой очень полезные хуки useMediaQuery, useWindowSize и другие. О первых двух можно прочитать в статье от Timeweb Cloud

Статья была полезной?