useLocalStorage и useSessionStorage в Mantine
Каждый раз, когда пользователь обновляет вкладку, React обнуляет все хуки состояния — и выбранная тема, и незаконченный черновик комментария испаряются в никуда.
Библиотека Mantine закрывает эту проблему двумя хуками из пакета @mantine/hooks: useLocalStorage и useSessionStorage. Они позволяют работать с браузерным хранилищем так же просто, как с обычным useState. На этом уроке разберём, чем они отличаются друг от друга, как их подключить и какие подводные камни стоит обойти стороной.
Зачем нужны хуки для работы с хранилищем
Нативные localStorage и sessionStorage — это императивные API, которые умеют хранить только строки. Чтобы положить туда объект, его нужно вручную сериализовать через JSON.stringify, а при чтении — не забыть про JSON.parse и про то, что значения может не быть вовсе. Хуже того, изменение значения в хранилище само по себе не вызывает повторный рендер компонента — для этого пришлось бы дублировать данные в обычном useState и вручную синхронизировать оба источника при каждом изменении.
Хуки Mantine убирают весь этот шаблонный код. С их точки зрения работа с хранилищем выглядит ровно так же, как работа с обычным состоянием: вы получаете пару [значение, функция обновления], а сериализация, чтение при монтировании и обработка особых случаев вроде отсутствия window на сервере уже встроены внутрь.
Вот лишь несколько сценариев, где эти хуки экономят реальное время разработки.
- Тема оформления интерфейса — светлая или тёмная
- Выбранный пользователем язык или регион
- Черновик комментария или письма, который жалко потерять при случайном обновлении страницы
- Состояние «свернуто/развёрнуто» боковой панели или блока фильтров
- Промежуточные шаги многошаговой формы или мастера настройки
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}
</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</p>
<button onClick={() => setStep((s) => Math.min(s + 1, 3))}>
Далее
</button>
</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 и без лишних перерендеров. Коротко о главном:
useLocalStorageхранит значение до тех пор, пока пользователь или код сам его не удалитuseSessionStorageживёт, пока открыта конкретная вкладка браузера- Оба хука работают как обычный
useState, но сериализуют данные черезserialize/deserialize removeValueсбрасывает значение кdefaultValueи очищает запись в хранилищеreadLocalStorageValueиreadSessionStorageValueпозволяют прочитать данные без подключения хука
В следующем уроке разберём ещё три полезных хука — useDebouncedValue, useMediaQuery и useClipboard — и научимся откладывать обновления состояния, реагировать на размер экрана и работать с буфером обмена.