Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage

RedDeveloper
20.04.2023 13:22
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage

Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента вещей, таких как токен авторизации или простой флаг для хранения предпочтений пользователя в темном режиме. Именно здесь на помощь приходит локальное хранилище .

Проблема:

К сожалению, локальное хранилище по умолчанию не вписывается в модель потока данных в react. То есть, в react нет встроенного механизма, который можно использовать для обновления состояния компонента в случае изменения значения локального хранилища или обновления локального хранилища в случае обновления состояния.

Решение:

Нам нужен пользовательский хук, подобный встроенному useState, который предоставляет значение и метод-установщик для этого значения. Он также должен обрабатывать события хранения.

Пример использования может быть следующим:

function LoginPage() {
  const [token, setToken] = useLocalStorage('token');

  const handleSubmit = () => {
    //...
    Login(credentials)
      .then(resp => {
        //Login successfull, save the token
        setToken(resp.token);
      });
  }
}
function useAuth() {
  const [token] = useLocalStorage('token');

  if (token) {
    //decode the jwt token and return the retrieved user object
  }

  return null;
}
function NavBar() {
  const loggedInUser = useAuth();
  
  return //...
         {loggedInUser && <Logout />}
         //...
}

В приведенном выше примере вызов setToken после успешного входа в систему сохранит новый токен в локальном хранилище и вызовет повторное отображение в компоненте NavBar, в результате чего появится кнопка выхода из системы. Потому что компонент NavBar использует пользовательский хук useAuth, который использует наш пользовательский хук useLocalStorage для создания возвращаемого значения.

В приведенном выше примере вызов setToken после успешного входа в систему сохранит новый
import { useCallback, useEffect, useState } from 'react';

type useLocalStorageFn = (key: string, initialValue?: string) => [string | null, (newValue: string) => void, () => void];

const useLocalStorage: useLocalStorageFn = (key, initialValue) => {
  const setLocalStorage = useCallback((newValue: string) => {
    localStorage.setItem(key, newValue);
    /*
     * Since localStorage.setItem only dispatches the event to other tabs,
     * we need to manually dispatch the event so the event listeners on the
     * same tab can also catch the event
     */
    window.dispatchEvent(
      new StorageEvent('storage', {
        storageArea: localStorage,
        newValue,
        key,
      }),
    );
  }, []);

  const [storage, setStorage] = useState(() => {
    /*
     * If a string is provided as initial value
     * update localStorage with that value
     * and return it as the initial value
     */
    if (typeof initialValue === 'string') {
      setLocalStorage(initialValue);
      return initialValue;
    }
    /*
     * If not return the already existing value from the localStorage
     * as the initial value
     */
    return localStorage.getItem(key);
  });

  /*
   * Setter method that will be returned from this hook
   * for updating value of the given key
   */
  const updateState = useCallback(
    (newValue: string) => {
      setLocalStorage(newValue);
      setStorage(newValue);
    },
    [setLocalStorage, setStorage],
  );

  // Remove the given key from localStorage
  const clearKey = useCallback(() => {
    localStorage.removeItem(key);
    window.dispatchEvent(
      new StorageEvent('storage', {
        storageArea: localStorage,
        newValue: null,
        key,
      }),
    );
  }, []);

  const cb = useCallback(
    function (event: StorageEvent) {
      if (event.storageArea === localStorage && event.key === key) {
        /*
         * Catch any localStorage event for given key
         * and update internal state with the new value
         */
        setStorage(event.newValue);
      }
    },
    [],
  );

  useEffect(() => {
    window.addEventListener('storage', cb);

    return () => {
      window.removeEventListener('storage', cb);
    };
  }, [cb]);

  return [storage, updateState, clearKey];
};

export default useLocalStorage;

Теперь нужно написать несколько тестов для хука:

import { fireEvent, renderHook, waitFor } from '@testing-library/react';
import useLocalStorage from '../src';

beforeEach(() => {
  localStorage.clear();
});

test('put given initial value to localStorage and return it as the default value', async () => {
  const { result } = renderHook(() => useLocalStorage('name', 'newValue'));

  const storageValue = localStorage.getItem('name');

  expect(result.current[0]).toBe('newValue');
  expect(storageValue).toBe('newValue');
});

test('return existing value from the storage as the default value', async () => {
  localStorage.setItem('name', 'newValue');

  const { result } = renderHook(() => useLocalStorage('name'));

  expect(result.current[0]).toBe('newValue');
});

test("return null as the default value if the given key doesn't exist in the storage", async () => {
  const { result } = renderHook(() => useLocalStorage('name'));

  expect(result.current[0]).toBe(null);
});

test('clear method should successfully remove the key from storage', async () => {
  localStorage.setItem('name', 'newValue');

  const { result: firstHook } = renderHook(() => useLocalStorage('name'));
  const { result: secondHook } = renderHook(() => useLocalStorage('name'));

  expect(firstHook.current[0]).toBe('newValue');

  await waitFor(() => firstHook.current[2]());

  expect(firstHook.current[0]).toBe(null);
  expect(secondHook.current[0]).toBe(null);
  expect(localStorage.getItem('name')).toBe(null);
});

test('sync state for the same key across different hook calls', async () => {
  const { result: firstHook } = renderHook(() => useLocalStorage('name', 'newValue'));
  const { result: secondHook } = renderHook(() => useLocalStorage('name'));

  expect(firstHook.current[0]).toBe('newValue');
  expect(secondHook.current[0]).toBe('newValue');

  await waitFor(() => firstHook.current[1]('test'));

  expect(secondHook.current[0]).toBe('test');
  expect(localStorage.getItem('name')).toBe('test');
});

test('successfully catch storage events', async () => {
  const { result } = renderHook(() => useLocalStorage('name', 'newValue'));

  expect(result.current[0]).toBe('newValue');

  fireEvent(
    window,
    new StorageEvent('storage', {
      storageArea: localStorage,
      newValue: 'updatedValue',
      key: 'name',
    }),
  );

  expect(result.current[0]).toBe('updatedValue');
});

А вот и наш крючок в действии:

А вот и наш крючок в действии

Вы можете посмотреть исходный код здесь , или установить его из npm.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?

20.08.2023 18:21

Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией

20.08.2023 17:46

В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.

Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox

19.08.2023 18:39

Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest

19.08.2023 17:22

В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!

Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️

18.08.2023 20:33

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

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL

14.08.2023 14:49

Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.