Блог

2025 / piscodev / Игорь Быстрицкий

обложка статьи
Блог

Valtio - удобное управление состоянием в React и Vanilla

Проще и быстрее, чем Redux, тот же функционал. Универсальный стейт менеджер для любого фреймворка: React, Next, Vue, Svelte и другие.

    • ReactОбщее
  • Нормально

  • 29 октября 2023
  • Игорь Быстрицкий

ЗнакомствоБыстрый стартУстановкаПервое хранилищеПодписка на обновления хранилищаГлобальное хранилищеПример со счетчикомИспользование в NextСохранение в local storageИтоги
Если понравилась статья, поделитесь с друзьями

Общее / Больше статей

Оформление UL OL списков с правильными знаками пунктуации

Оформление UL OL списков с правильными знаками пунктуации

Как правильно оформлять списки (перечни) в Word, Google Docs, Markdown и др. Когда начинать новый пункт списка с заглавной буквы, а когда со строчной? Когда после каждого пункта ставить запятую, точку и точку с запятой? Научитесь подбирать правильные знаки препинания в зависимости от ситуации. Полный гайд. Обязательно к прочтению студентам для написания курсовых и выпускных работ, редакторам для написания статей.

11 ноября 2023

Знакомство

Стейт менеджер (State Manager / Global State Manager) - это инструмент, который позволяет создать глобальное хранилище переменных, которое доступно для чтения и изменения в любом уголке приложения. Стейт менеджеры используются почти на всех сайтах, где существует минимальная интерактивность, потому что они очень удобны.

Redux был моим первым стейт менеджером и я быстро смог изучить и полюбить его.

Однако время идет и в 2023 году Редакс очень надоел. Его слайсы и редьюсеры звучат как заболевание, а громоздкий API и огромное количество бойлерплейта засоряют код и затрудняют разработку. На этой почве в 2021 году у Редакса появляется мощный конкурент - Valtio.

Valtio - это независимый от фреймворка стейт мененджер.

Valtio можно использовать даже в ванильном проекте Ноды, но лучше, конечно же, установить Valtio в React, чтобы использовать кастомный хук useSnapshot, который самостоятельно возвращает актуальные значения из хранилища.

Быстрый старт

За полной документацией беги на официальный сайт Valtio.

Установка

Bash

npm i valtio

Первое хранилище

Для создания хранилища (store) нужно лишь обернуть желаемый объект в функцию proxy. В следующем примере я создам простое хранилище с выбранной темой интерфейса:

TypeScript

// @/utils/store.ts

import { proxy } from 'valtio'

// Тип для удобства.
type TStore = {
	theme: 'dark' | 'light'
}

// Стандартные значения хранилища.
const defaultStore: TStore = {
	theme: 'dark',
}

// Функция proxy оборачивает defaultStore и создает из него хранилище. Это хранилище надо экспортировать, чтобы обращаться к нему напрямую для изменения данных.
export const store = proxy(defaultStore)
Хранилище готово 👍.
Так как store является константой, важно передавать в proxy именно объект, чтобы можно было изменять его свойства.

Значения изменяются по-ванильному. Никаких дополнительных функций:

TypeScript

// Так просто. Все подписчики theme получат новое значение light.
store.theme = 'light'

Подписка на обновления хранилища

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

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

TypeScript JSX

// @/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>
}
useSnapshot испольуется только для чтения. Все его переменные имеют префикс readonly. Для изменения значений хранилища нужно обращаться к прокси-объекту store. Таковы правила Valtio.

Глобальное хранилище

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

TypeScript

// @/utils/store.ts

export function useGlobalSnapshot() {
	return useSnapshot(store)
}

TypeScript

// Было.
const { theme } = useSnapshot(store)

// Стало.
const { theme } = useGlobalSnapshot()

Пример со счетчиком

TypeScript JSX

// @/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>
	)
}

Использование в Next

Так как Valtio работает на клиенте, файлу store.ts нужно добавить директиву 'use client':

'use client' должен быть на 1 строчке. Это требование Next.

TypeScript

'use client'

...

export store = proxy({
  ...
})

Сохранение в local storage

В Valtio есть функция subscribe, которая реагирует на все обновления хранилища. С ее помощью приложение будет записывать хранилище в виде объекта в локальное хранилище:

TypeScript

// @/utils/store.ts

import { subscribe, snapshot } from 'valtio'

// функция subscribe слушает store и выполняет колбек.
// функция snapshot аналогична хуку useSnapshot, но работает как обычная функция. Поэтому здесь, не в реакт компоненте, нужно использовать именно ее.
subscribe(store, () => {
	localStorage.setItem('store', JSON.stringify(snapshot(store)))
})

Теперь хранилище будет сохраняться между перезагрузками в локальном хранилище. Пока что приложени не видит его. Нужно создать хук-инициализатор, который будет читать хранилище из local storage при первой загрузке и загружать его в приложение.

TypeScript JSX

// @/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:

TypeScript JSX

export default function App() {
	// Вызов инициализатора:
	StoreInitializer()

	return <main>...</main>
}
Так приложение загрузит данные из локального хранилище в store.

Итоги

Valtio - это очень простой стейт менеджер. Я использую его на постоянной основе как замену Redux и советую использовать его тебе тоже.