Настройка State Management (Jotai) для React-приложения
Jotai строит состояние из атомов — маленьких изолированных единиц, которые можно комбинировать. Ближайшая аналогия — Recoil, но без boilerplate и с лучшей TypeScript-поддержкой из коробки. Компонент подписывается только на те атомы, которые читает, и ре-рендерится только при их изменении.
Хорошо подходит для приложений с большим количеством независимых кусков состояния: форм, UI-состояния, кэша запросов.
Что входит в работу
Настройка атомарной структуры состояния под проект: базовые атомы, производные атомы, async атомы, интеграция с React Suspense, подключение devtools, организация файлов.
Установка
npm install jotai
# опционально — утилиты
npm install jotai-devtools
Базовые атомы
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
// примитивный атом
export const countAtom = atom(0)
// производный атом (readonly)
export const doubleCountAtom = atom((get) => get(countAtom) * 2)
// производный атом с записью
export const incrementAtom = atom(
(get) => get(countAtom),
(get, set, step: number = 1) => set(countAtom, get(countAtom) + step)
)
function Counter() {
const [count, setCount] = useAtom(countAtom)
const double = useAtomValue(doubleCountAtom)
const increment = useSetAtom(incrementAtom)
return (
<div>
<span>{count} (×2 = {double})</span>
<button onClick={() => increment(1)}>+1</button>
<button onClick={() => setCount(0)}>Сброс</button>
</div>
)
}
useAtomValue — подписка только на чтение, не возвращает сеттер. useSetAtom — только сеттер, компонент не ре-рендерится при изменении атома.
Async атомы и Suspense
import { atom } from 'jotai'
const userIdAtom = atom<number | null>(null)
// async производный атом
export const userAtom = atom(async (get) => {
const id = get(userIdAtom)
if (!id) return null
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error('Пользователь не найден')
return res.json() as Promise<User>
})
// компонент оборачивается в Suspense
function UserProfile() {
const user = useAtomValue(userAtom) // suspend до загрузки
if (!user) return <p>Выберите пользователя</p>
return <p>{user.name}</p>
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<ErrorBoundary fallback={<Error />}>
<UserProfile />
</ErrorBoundary>
</Suspense>
)
}
Атом с записью для форм
interface FormState {
name: string
email: string
errors: Record<string, string>
}
const formAtom = atom<FormState>({
name: '',
email: '',
errors: {},
})
// производный атом-экшен
export const submitFormAtom = atom(null, async (get, set) => {
const form = get(formAtom)
const errors: Record<string, string> = {}
if (!form.name) errors.name = 'Обязательное поле'
if (!form.email.includes('@')) errors.email = 'Некорректный email'
if (Object.keys(errors).length > 0) {
set(formAtom, { ...form, errors })
return
}
await api.post('/users', { name: form.name, email: form.email })
set(formAtom, { name: '', email: '', errors: {} })
})
Atomization — дробление объектов
Jotai позволяет разделить один объект на независимо обновляемые атомы:
import { splitAtom } from 'jotai/utils'
const todosAtom = atom<Todo[]>([
{ id: 1, text: 'Купить молоко', done: false },
{ id: 2, text: 'Написать тесты', done: true },
])
export const todoAtomsAtom = splitAtom(todosAtom)
function TodoList() {
const [todoAtoms] = useAtom(todoAtomsAtom)
return (
<ul>
{todoAtoms.map((todoAtom) => (
<TodoItem key={`${todoAtom}`} atom={todoAtom} />
))}
</ul>
)
}
function TodoItem({ atom }: { atom: PrimitiveAtom<Todo> }) {
const [todo, setTodo] = useAtom(atom)
// ре-рендерится только при изменении этого конкретного todo
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={(e) => setTodo({ ...todo, done: e.target.checked })}
/>
{todo.text}
</li>
)
}
Персистентность
import { atomWithStorage } from 'jotai/utils'
export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light')
export const tokenAtom = atomWithStorage<string | null>('auth_token', null)
atomWithStorage работает с localStorage, sessionStorage или любым кастомным хранилищем.
Provider и изолированные scope
По умолчанию атомы — глобальные синглтоны. Для изоляции (тесты, storybook, несколько независимых экземпляров):
import { createStore, Provider } from 'jotai'
const storeA = createStore()
const storeB = createStore()
storeA.set(countAtom, 10)
storeB.set(countAtom, 20)
function App() {
return (
<>
<Provider store={storeA}><Counter /></Provider>
<Provider store={storeB}><Counter /></Provider>
</>
)
}
DevTools
import { DevTools } from 'jotai-devtools'
import 'jotai-devtools/styles.css'
function App() {
return (
<>
{process.env.NODE_ENV === 'development' && <DevTools />}
<Router />
</>
)
}
Отображает дерево атомов, текущие значения, историю изменений.
Структура файлов
src/
atoms/
auth.atom.ts
ui.atom.ts # тема, язык, sidebar open/close
cart.atom.ts
derived/ # производные и async атомы
user.atom.ts
products.atom.ts
Что делаем
Аудит текущего состояния в приложении, перевод на атомарную модель, настройка async атомов с Suspense там, где нужна стриминговая загрузка, подключение devtools, написание тестов атомов через createStore().
Срок: 1–2 дня.







