Bash
npm i valtio
Стейт менеджер (State Manager / Global State Manager) - это инструмент, который позволяет создать глобальное хранилище переменных, которое доступно для чтения и изменения в любом уголке приложения. Стейт менеджеры используются почти на всех сайтах, где существует минимальная интерактивность, потому что они очень удобны.
Redux был моим первым стейт менеджером и я быстро смог изучить и полюбить его.
Однако время идет и в 2023 году Редакс очень надоел. Его слайсы и редьюсеры звучат как заболевание, а громоздкий API и огромное количество бойлерплейта засоряют код и затрудняют разработку. На этой почве в 2021 году у Редакса появляется мощный конкурент - Valtio.
Valtio - это независимый от фреймворка стейт мененджер.
npm i valtio
Для создания хранилища (store) нужно лишь обернуть желаемый объект в функцию proxy
. В следующем примере я создам простое хранилище с выбранной темой интерфейса:
// @/utils/store.ts
import { proxy } from 'valtio'
// Тип для удобства.
type TStore = {
theme: 'dark' | 'light'
}
// Стандартные значения хранилища.
const defaultStore: TStore = {
theme: 'dark',
}
// Функция proxy оборачивает defaultStore и создает из него хранилище. Это хранилище надо экспортировать, чтобы обращаться к нему напрямую для изменения данных.
export const store = proxy(defaultStore)
Значения изменяются по-ванильному. Никаких дополнительных функций:
// Так просто. Все подписчики theme получат новое значение light.
store.theme = 'light'
Так как в своем блоге я скрыто пропагандирую React, поэтому буду использовать кастомный хук useSnapshot
, чтобы подписаться на этот прокси-объект и получать свежие данные.
// @/components/theme-button.tsx
import { useSnapshot } from 'valtio'
import { store } from '@/utils/store'
import { BiSun, BiMoon } from '@/assets/icons'
// Кнопка, которая переключает тему.
export default function ThemeButton() {
// Кнопка получает и слушает переменную theme в хранилище store.
const {theme} = useSnapshot(store)
return <button>{theme === 'light' ? <BiSun/> : <BiMoon>}</button>
}
Обычно хранилище создается одно на весь проект ради удобства и чтобы не запоминать, в каком хранилище находится какая переменная. Поэтому можно упростить себе работу и создать хук для чтения глобального хранилища:
// @/utils/store.ts
export function useGlobalSnapshot() {
return useSnapshot(store)
}
// Было.
const { theme } = useSnapshot(store)
// Стало.
const { theme } = useGlobalSnapshot()
// @/components/counter.tsx
import { proxy, useSnapshot } from 'valtio'
// Создам хранилище прямо здесь, никто не запрещает.
// Отмечу, что оно все равно будет создано 1 раз и останется единым, сколько бы Counter не было отрендерно.
const counterStore = proxy({
count: 0,
})
export default function Counter() {
const { count } = useSnapshot(counterStore)
return (
<div>
<p>Текущее число: {count}</p>
<button onClick={() => (counterStore.count = count + 1)}>Больше</button>
<button onClick={() => (counterStore.count = count - 1)}>Меньше</button>
<button onClick={() => (counterStore.count = 0)}>Сбросить</button>
</div>
)
}
Так как Valtio работает на клиенте, файлу store.ts
нужно добавить директиву 'use client'
:
'use client'
...
export store = proxy({
...
})
В Valtio есть функция subscribe
, которая реагирует на все обновления хранилища. С ее помощью приложение будет записывать хранилище в виде объекта в локальное хранилище:
// @/utils/store.ts
import { subscribe, snapshot } from 'valtio'
// функция subscribe слушает store и выполняет колбек.
// функция snapshot аналогична хуку useSnapshot, но работает как обычная функция. Поэтому здесь, не в реакт компоненте, нужно использовать именно ее.
subscribe(store, () => {
localStorage.setItem('store', JSON.stringify(snapshot(store)))
})
Теперь хранилище будет сохраняться между перезагрузками в локальном хранилище. Пока что приложени не видит его. Нужно создать хук-инициализатор, который будет читать хранилище из local storage при первой загрузке и загружать его в приложение.
// @/utils/store-initialzier.tsx
'use client'
export const StoreInitializer = () => {
useEffect(() => {
// Проверка, существует ли хранилище:
if (localStorage.getItem('store')) {
const storageStore = JSON.parse(localStorage.getItem('store')!) as TStore
// Так как store является константой, перезаписывать его нужно через Object:
Object.assign(store, storageStore)
}
}, [])
Чтобы этот хук вызывался при первом запуске приложения из какой бы страницы юзер не открыл сайт, этот хук нужно засунуть в компонент самого высокого уровня, например App
:
export default function App() {
// Вызов инициализатора:
StoreInitializer()
return <main>...</main>
}
Valtio - это очень простой стейт менеджер. Я использую его на постоянной основе как замену Redux и советую использовать его тебе тоже.