Локальное хранилище — это хорошее место, обычно используемое для хранения данных (но не токенов аутентификации!), которые необходимо сохранять между сеансами.

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

Из официальных документов:

useSyncExternalStore — это React Hook, который позволяет вам подписаться на внешний магазин.

В нашем контексте внешнее хранилище относится к локальному хранилищу. useSyncExternalStore позволяет нам устранить разрыв между React и локальным хранилищем путем подписки компонента на локальное хранилище.

Пример с useState + useEffect

Давайте сначала рассмотрим пример плохой практики, которая не работает должным образом ❌:

Код, который используется в примере выше:

import React from "react";

type SidebarState = "collapsed" | "expanded";

const get = () => localStorage.getItem("sidebar") as SidebarState;
const set = (value: SidebarState) => localStorage.setItem("sidebar", value);

if (!get()) {
 set("collapsed");
}

function App() {
 const [sidebarState, setSidebarState] = React.useState<SidebarState>(get());

 React.useEffect(() => {
  set(sidebarState);
 }, [sidebarState]);

 const handleToggle = () =>
  setSidebarState(sidebarState === "collapsed" ? "expanded" : "collapsed");

 return (
  <>
   <p>
    The sidebar is{" "}
    <span style={{ color: sidebarState === "collapsed" ? "red" : "green" }}>
     {sidebarState}
    </span>
   </p>
   <button onClick={handleToggle}>Toggle State</button>
  </>
 );
}
  1. Мы используем локальное хранилище только в качестве начального состояния для React useState. Это необходимо для достижения реактивности, иначе React не будет обновлять пользовательский интерфейс (повторно отображать) при прямых обновлениях в локальном хранилище.
  2. useState означает, что состояние связано с этим экземпляром компонента, а это значит, что оно не будет синхронизироваться между вкладками.
  3. Наконец, useState означает, что нам необходимо синхронизировать локальное хранилище с изменениями, чтобы при следующей загрузке приложения мы получили последнее установленное состояние.

Пример с использованиемSyncExternalStore

Правильный способ сделать это 💪

Код, который используется в примере выше:

import React from "react";

type SidebarState = "collapsed" | "expanded";

function setSidebarState(newValue: SidebarState) {
 window.localStorage.setItem("sidebar", newValue);
 // On localStoage.setItem, the storage event is only triggered on other tabs and windows.
 // So we manually dispatch a storage event to trigger the subscribe function on the current window as well.
 window.dispatchEvent(
  new StorageEvent("storage", { key: "sidebar", newValue })
 );
}

const store = {
 getSnapshot: () => localStorage.getItem("sidebar") as SidebarState,
 subscribe: (listener: () => void) => {
  window.addEventListener("storage", listener);
  return () => void window.removeEventListener("storage", listener);
 },
};

// Set the initial value.
if (!store.getSnapshot()) {
 localStorage.setItem("sidebar", "collapsed" satisfies SidebarState);
}

function App() {
 const sidebarState = React.useSyncExternalStore(
  store.subscribe,
  store.getSnapshot
 );

 const handleToggle = () => {
  setSidebarState(sidebarState === "expanded" ? "collapsed" : "expanded");
 };

 return (
  <>
   <p>
    The sidebar is
    <span style={{ color: sidebarState === "collapsed" ? "red" : "green" }}>
     {sidebarState}
    </span>
   </p>
   <button onClick={handleToggle}>Toggle State</button>
  </>
 );
}

useSyncExternalStore принимает два обязательныхаргумента:

  1. Функция subscribe должна подписаться на хранилище и вернуть функцию, которая отписывается. Аргумент listener в этой функции автоматически прослушивает события storage и повторно отображает компонент при изменениях.
  2. Функция getSnapshot должна прочитать снимок данных из хранилища. Чтобы упростить задачу, вам следует избегать возврата неизменяемых данных (например, объектов), поскольку они различаются при каждом вызове getSnapshot и будут вызывать бесконечные повторные рендеринги. Если вам необходимо, вам следует кэшировать возвращаемое значение getSnapshot.

Эти две функции подключают данные, хранящиеся в локальном хранилище, к React и обеспечивают реактивность между вкладками и окнами.

Бонус: извлеките хранилище в специальный хук.

Наконец, вы можете извлечь логику в собственный хук:

import React from "react";

type SidebarState = "collapsed" | "expanded";

function useSidebarState() {
 const setSidebarState = (newValue: SidebarState) => {
  window.localStorage.setItem("sidebar", newValue);
  window.dispatchEvent(
   new StorageEvent("storage", { key: "sidebar", newValue })
  );
 };

 const getSnapshot = () => localStorage.getItem("sidebar") as SidebarState;

 const subscribe = (listener: () => void) => {
  window.addEventListener("storage", listener);
  return () => void window.removeEventListener("storage", listener);
 };

 const store = React.useSyncExternalStore(subscribe, getSnapshot);

 return [store, setSidebarState] as const;
}

Краткое содержание

Сегодня мы узнали, что комбинация useState и useEffect не является идеальным способом управления состоянием с использованием данных, находящихся в локальном хранилище. Мы можем использовать локальное хранилище в качестве внешнего хранилища, которое взаимодействует с React с помощью useSyncExternalStore.

Давайте соединимся

Вы можете связаться со мной по следующим ссылкам:

  1. Линкедин
  2. ГитХаб

Если вы хотите видеть больше контента о комплексном проектировании, не забудьте поставить лайк и подписаться, чтобы выразить свою поддержку! Спасибо!