Хотите заказать веб-сайт? Связаться с нами

useLocalStorage и useSessionStorage в Mantine

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

Библиотека Mantine закрывает эту проблему двумя хуками из пакета @mantine/hooks: useLocalStorage и useSessionStorage. Они позволяют работать с браузерным хранилищем так же просто, как с обычным useState. На этом уроке разберём, чем они отличаются друг от друга, как их подключить и какие подводные камни стоит обойти стороной.

useLocalStorage и useSessionStorage в Mantine

Зачем нужны хуки для работы с хранилищем

Нативные localStorage и sessionStorage — это императивные API, которые умеют хранить только строки. Чтобы положить туда объект, его нужно вручную сериализовать через JSON.stringify, а при чтении — не забыть про JSON.parse и про то, что значения может не быть вовсе. Хуже того, изменение значения в хранилище само по себе не вызывает повторный рендер компонента — для этого пришлось бы дублировать данные в обычном useState и вручную синхронизировать оба источника при каждом изменении.

Хуки Mantine убирают весь этот шаблонный код. С их точки зрения работа с хранилищем выглядит ровно так же, как работа с обычным состоянием: вы получаете пару [значение, функция обновления], а сериализация, чтение при монтировании и обработка особых случаев вроде отсутствия window на сервере уже встроены внутрь.

Вот лишь несколько сценариев, где эти хуки экономят реальное время разработки.

  1. Тема оформления интерфейса — светлая или тёмная
  2. Выбранный пользователем язык или регион
  3. Черновик комментария или письма, который жалко потерять при случайном обновлении страницы
  4. Состояние «свернуто/развёрнуто» боковой панели или блока фильтров
  5. Промежуточные шаги многошаговой формы или мастера настройки

useLocalStorage: постоянное состояние между сессиями

Хук useLocalStorage принимает один объект настроек и возвращает кортеж, похожий на результат useState, но с третьим элементом — функцией удаления значения. Единственный обязательный параметр — key, ключ, под которым данные будут лежать в браузере. Остальные параметры — defaultValue, serialize, deserialize и getInitialValueInEffect — опциональны и покрывают почти любой практический случай.

Базовое использование

Самый простой пример — переключатель темы, значение которого должно «запоминаться» между визитами пользователя на сайт.

import { useLocalStorage } from '@mantine/hooks';
 
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>({
    key: 'app-theme',
    defaultValue: 'light',
  });
 
  const toggleTheme = () =>
    setTheme((current) => (current === 'dark' ? 'light' : 'dark'));
 
  return (
    <button onClick={toggleTheme}>
      Текущая тема: {theme}
    &lt;/button>
  );
}

При каждом вызове setTheme хук одновременно обновляет React-состояние и вызывает localStorage.setItem. При следующей загрузке страницы значение автоматически считывается обратно через localStorage.getItem и проходит через JSON.parse — писать это руками не нужно.

Своя сериализация и десериализация

По умолчанию хук сериализует данные через JSON.stringify/JSON.parse, и для примитивов, массивов и простых объектов этого хватает с запасом. Но как только в состоянии появляются Date, Map, Set или экземпляры классов, стандартный JSON.stringify либо теряет часть данных, либо падает с ошибкой. Для таких случаев в объект настроек можно передать собственные функции serialize и deserialize.

import { useLocalStorage } from '@mantine/hooks';
 
interface UserPrefs {
  fontSize: number;
  language: string;
}
 
const defaultPrefs: UserPrefs = { fontSize: 16, language: 'ru' };
 
const [prefs, setPrefs] = useLocalStorage<UserPrefs>({
  key: 'user-prefs',
  defaultValue: defaultPrefs,
  serialize: (value) => JSON.stringify(value),
  deserialize: (value) => (value ? JSON.parse(value) : defaultPrefs),
});

Если формат данных сложнее, чем поддерживает обычный JSON, стоит взглянуть на сторонние сериализаторы вроде superjson — они подключаются в те же параметры serialize/deserialize без изменения остального кода. Для типизации собственных обёрток над хуком Mantine экспортирует типы UseStorageOptions и UseStorageReturnValue.

Что если localStorage недоступен

На сервере при серверном рендеринге объекта window попросту не существует, поэтому хук возвращает defaultValue до тех пор, пока компонент не примонтируется в браузере. Похожая ситуация возникает в приватных вкладках Safari, где вызов localStorage.setItem может выбросить исключение из-за ограничений браузера. Mantine оборачивает обращения к хранилищу в безопасные проверки, так что приложение не падает — оно просто продолжает работать с значением в памяти, не сохраняя его между сессиями.

Не спешите отключать getInitialValueInEffect, чтобы получить сохранённое значение сразу при первом рендере. По умолчанию хук читает localStorage только внутри useEffect — это сделано намеренно, чтобы разметка на сервере и при первом рендере в браузере совпадала. Если вывод компонента зависит от значения из хранилища, отключение этой опции почти гарантированно приведёт к предупреждению о hydration mismatch в Next.js или другом SSR-фреймворке. Ставьте getInitialValueInEffect: false только в полностью клиентских приложениях без серверного рендеринга.

const [theme, setTheme] = useLocalStorage<'light' | 'dark'>({
  key: 'app-theme',
  defaultValue: 'light',
  getInitialValueInEffect: false,
});

useSessionStorage: хранение данных в пределах вкладки

Хук useSessionStorage устроен идентично useLocalStorage и принимает те же параметры — key, defaultValue, serialize, deserialize, getInitialValueInEffect. Разница только в том, где физически лежат данные: вместо window.localStorage используется window.sessionStorage. Это значит, что значение доживает до закрытия вкладки (обновление страницы его не стирает), но пропадает, как только вкладка закрыта, и — в отличие от localStorage — никогда не расшаривается между разными вкладками одного и того же сайта.

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

import { useSessionStorage } from '@mantine/hooks';
 
function OnboardingWizard() {
  const [step, setStep] = useSessionStorage({
    key: 'onboarding-step',
    defaultValue: 1,
  });
 
  return (
    <div>
      <p>Шаг {step} из 3&lt;/p>
      <button onClick={() => setStep((s) => Math.min(s + 1, 3))}>
        Далее
      &lt;/button>
    &lt;/div>
  );
}

Под капотом оба хука используют одну и ту же реализацию и отличаются только тем, к какому объекту хранилища она обращается. Поэтому весь код из раздела про useLocalStorage — типизация значения, кастомная сериализация, обработка отсутствия хранилища — работает для useSessionStorage без изменений.

Критерий useLocalStorage useSessionStorage
Время жизни данных До явного удаления пользователем или кодом До закрытия вкладки браузера
Доступ из других вкладок Да, значение общее для всех вкладок сайта Нет, у каждой вкладки своя копия
Типичный сценарий Тема оформления, язык интерфейса Шаги мастера, черновик формы на сессию

Удаление, синхронизация и чтение без хука

Удаление значения через removeValue

Третий элемент кортежа, который возвращают оба хука, — функция removeValue. Она стирает запись из хранилища и одновременно сбрасывает React-состояние обратно к defaultValue, поэтому подходит для кнопок «сбросить настройки» или для очистки данных при выходе пользователя из аккаунта.

import { useLocalStorage } from '@mantine/hooks';
 
const [draft, setDraft, removeDraft] = useLocalStorage({
  key: 'comment-draft',
  defaultValue: '',
});
 
function clearDraft() {
  removeDraft();
}

Синхронизация между вкладками

useLocalStorage подписывается на нативное браузерное событие storage. Это значит, что если пользователь поменяет тему в одной открытой вкладке, все остальные вкладки с этим же хуком мгновенно перерисуются с новым значением — без перезагрузки и без дополнительного кода с вашей стороны. Хуку useSessionStorage такая подписка не нужна: у каждой вкладки и так своё изолированное хранилище, синхронизировать между ними нечего.

Чтение значения без хука

Иногда значение из хранилища требуется в обычной функции вне React-компонента — например, в утилите, которая настраивает конфигурацию ещё до первого рендера приложения. Для таких случаев Mantine экспортирует автономные функции readLocalStorageValue и readSessionStorageValue: они принимают тот же параметр key, что и хуки, но возвращают значение один раз, без подписки на дальнейшие изменения.

import { readLocalStorageValue } from '@mantine/hooks';
 
const cachedTheme = readLocalStorageValue<'light' | 'dark'>({ key: 'app-theme' });

Не превращайте localStorage в свалку для всего подряд. Значение, которое можно вычислить из других данных приложения — например, отфильтрованный список, — не стоит дублировать в хранилище: держите там только то, что действительно нужно восстановить после перезагрузки. Полезно также добавлять к ключам общий префикс вроде app:theme или app:onboarding-step — это убережёт от случайных коллизий, если на странице уживаются несколько независимых виджетов или сторонних скриптов, которые тоже пишут в браузерное хранилище.

Робот собирает из блоков сайт

Собери свой код. Запусти сайт!

От наброска на салфетке до первого работающего лендинга. Наш онлайн-курс «Веб-верстка с нуля и до профессионала» — это интенсивный трек, где ты не будешь зубрить теорию, а с первого дня начнешь превращать идеи в чистый HTML и CSS.

Собери свой первый проект под руководством практикующих разработчиков.

Подробнее о курсе

Итоги

Хуки useLocalStorage и useSessionStorage из @mantine/hooks превращают работу с браузерным хранилищем в обычную работу со state — без ручного JSON.stringify, без забытых проверок на существование window и без лишних перерендеров. Коротко о главном:

  1. useLocalStorage хранит значение до тех пор, пока пользователь или код сам его не удалит
  2. useSessionStorage живёт, пока открыта конкретная вкладка браузера
  3. Оба хука работают как обычный useState, но сериализуют данные через serialize/deserialize
  4. removeValue сбрасывает значение к defaultValue и очищает запись в хранилище
  5. readLocalStorageValue и readSessionStorageValue позволяют прочитать данные без подключения хука

В следующем уроке разберём ещё три полезных хука — useDebouncedValue, useMediaQuery и useClipboard — и научимся откладывать обновления состояния, реагировать на размер экрана и работать с буфером обмена.

Теги: