Recoil — атомарный state manager от Meta, который даёт точечный контроль над ре-рендерами. Мы внедряем Recoil в крупные React-приложения, где производительность критична. В отличие от Redux, здесь нет единого стора — атомы и селекторы образуют граф зависимостей. Это позволяет обновлять только те компоненты, которые действительно зависят от изменившихся данных. На практике такой подход сокращает количество лишних рендеров на 30–50% и упрощает отладку.
Мы настроим RecoilRoot, спроектируем атомную архитектуру под ваш домен, реализуем асинхронные запросы через Suspense и добавим персистентность через атомные эффекты. Опыт наших инженеров — 5+ лет работы с React и state management. Гарантируем, что после интеграции вы получите предсказуемое и быстрое обновление UI, а также существенно снизите издержки на инфраструктуру за счёт сокращения времени рендеринга.
Как Recoil улучшает производительность React?
Recoil используют технику fine-grained subscriptions: каждый компонент подписывается только на те атомы, от которых он реально зависит. Когда меняется один атом, Recoil перерисовывает только подписанные компоненты, а не всё поддерево. Это особенно эффективно для списков, форм и панелей с большим количеством интерактивных элементов.
Сравним с Redux: там изменение стора запускает connect во всех подключённых компонентах, и только shallowEqual решает, обновляться ли. В Recoil обновление идёт по графу, что даёт выигрыш в скорости до 2 раз при пятисотсвязном состоянии. Асинхронные данные загружаются через селекторы, которые кэшируют результат до изменения зависимостей — это снижает количество запросов к API.
| Характеристика | Recoil | Redux (с Thunk) | Zustand |
|---|---|---|---|
| Рендер-оптимизация | Точечные подписки | connect/mapState | Селекторы вручную |
| Асинхронность | Встроенный async-selector | Middleware (Thunk/Saga) | Асинхронные actions |
| Размер бандла | ~15 KB | ~30 KB | ~3 KB |
| Кривая обучения | Низкая (2-3 часа) | Средняя (1-2 дня) | Низкая (1 час) |
Recoil в 2 раза легче Redux по размеру, а по скорости обновления сложных форм — в 1,5 раза быстрее при >1000 атомов. Это прямая экономия бюджета: меньше нагрузка на процессор и память.
Что входит в настройку Recoil?
- Проектирование атомов и селекторов под бизнес-логику.
- Интеграция с Suspense для асинхронных данных.
- Типизация атомов (TypeScript) и создание AtomFamily для списков.
- Настройка персистентности через localStorage или IndexedDB.
- Подключение devtools и написание тестов для критических селекторов.
- Документация по атомной архитектуре проекта.
Базовая настройка: установка и структура
Установите npm install recoil, затем оберните приложение в <RecoilRoot>:
import { RecoilRoot } from 'recoil'
function App() {
return (
<RecoilRoot>
<React.StrictMode>
<Router />
</React.StrictMode>
</RecoilRoot>
)
}
Документация Recoil рекомендует размещать RecoilRoot на самом верхнем уровне, чтобы хуки работали во всех компонентах.
Атомы, селекторы и хуки
Атом — единица состояния с уникальным ключом. Селекторы вычисляют производные данные с мемоизацией. Мы используем хуки: useRecoilState, useRecoilValue, useSetRecoilState, useResetRecoilState.
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil'
// Атомы
export const authTokenAtom = atom<string | null>({ key: 'authToken', default: null })
export const cartItemsAtom = atom<CartItem[]>({ key: 'cartItems', default: [] })
// Селекторы
export const cartTotalSelector = selector<number>({
key: 'cartTotal',
get: ({ get }) => get(cartItemsAtom).reduce((sum, item) => sum + item.price * item.quantity, 0)
})
// Хуки в компоненте
function CartBadge() {
const count = useRecoilValue(cartTotalSelector)
return <span>{count}</span>
}
function CartControls() {
const [items, setItems] = useRecoilState(cartItemsAtom)
const reset = useResetRecoilState(cartItemsAtom)
const addItem = (item: CartItem) => setItems(prev => {
const existing = prev.find(i => i.id === item.id)
if (existing) return prev.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i)
return [...prev, { ...item, quantity: 1 }]
})
return (
<>
<button onClick={() => addItem(product)}>Добавить</button>
<button onClick={reset}>Очистить корзину</button>
</>
)
}
Какие асинхронные данные можно загружать через Suspense?
Любые: профиль пользователя, список товаров, поисковые результаты. Создаём async-селектор, который возвращает Promise, и оборачиваем компонент в <Suspense>. Recoil сам обработает loading и error состояния.
import { selector, selectorFamily } from 'recoil'
// Параметризованный асинхронный селектор
export const productSelector = selectorFamily<Product, number>({
key: 'product',
get: (productId) => async () => {
const res = await fetch(`/api/products/${productId}`)
if (!res.ok) throw new Error(`Product ${productId} not found`)
return res.json()
},
})
// Использование
function ProductCard({ id }: { id: number }) {
const product = useRecoilValue(productSelector(id))
return <div>{product.name} — {product.price} ₽</div>
}
function ProductPage({ id }: { id: number }) {
return (
<Suspense fallback={<Skeleton />}>
<ErrorBoundary fallback={<NotFound />}>
<ProductCard id={id} />
</ErrorBoundary>
</Suspense>
)
}
AtomFamily для списков
AtomFamily создаёт отдельный атом для каждого элемента коллекции. Это удобно для todo-списков, форм в таблице и других динамических сущностей.
import { atomFamily, useRecoilState } from 'recoil'
// Для каждого ID — свой атом
export const todoAtomFamily = atomFamily<Todo, number>({
key: 'todo',
default: (id) => ({ id, text: '', done: false }),
})
function TodoItem({ id }: { id: number }) {
const [todo, setTodo] = useRecoilState(todoAtomFamily(id))
return (
<label>
<input type="checkbox" checked={todo.done} onChange={e => setTodo(t => ({ ...t, done: e.target.checked }))} />
{todo.text}
</label>
)
}
Персистентность через эффекты
AtomEffect позволяет сохранять состояние в localStorage, sessionStorage или IndexedDB. При инициализации эффект загружает сохранённое значение, при каждом изменении — записывает.
import { AtomEffect, atom } from 'recoil'
function localStorageEffect<T>(key: string): AtomEffect<T> {
return ({ setSelf, onSet }) => {
const saved = localStorage.getItem(key)
if (saved !== null) {
try { setSelf(JSON.parse(saved)) } catch {}
}
onSet((newValue, _, isReset) => {
if (isReset) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, JSON.stringify(newValue))
}
})
}
}
export const themeAtom = atom<'light' | 'dark'>({
key: 'theme',
default: 'light',
effects: [localStorageEffect('theme')],
})
| Метод персистентности | Размер хранилища | Синхронность | Поддержка JSON |
|---|---|---|---|
| localStorage | ~5–10 MB | Синхронно | Да |
| sessionStorage | ~5–10 MB | Синхронно | Да |
| IndexedDB | до 1 GB+ | Асинхронно | Да (ч/з обёртку) |
Типичные ошибки при работе с Recoil
- Забыли обернуть компонент в
<RecoilRoot>— хуки не работают. - Использование одного атома для всего состояния — теряется оптимизация.
- Селекторы с побочными эффектами — Recoil не следит за ними, нужны AtomEffect.
- Игнорирование типов — атому нужен TypeScript, иначе сложно отлаживать.
- Неверный ключ атома — приводит к пересечению данных.
Процесс и сроки внедрения
- Анализ текущего состояния (1 день) — разбираем граф зависимостей, определяем критичные атомы.
- Проектирование атомной архитектуры (1-2 дня) — создаём схему атомов, селекторов и эффектов.
- Реализация и интеграция (2-4 дня) — устанавливаем Recoil, переносим состояние, пишем тесты.
- Оптимизация и документирование (1 день) — настраиваем devtools, проверяем производительность.
Срок: от 3 до 7 рабочих дней в зависимости от объёма.
Получите консультацию по вашему проекту — наши инженеры оценят состояние и предложат решение. Свяжитесь с нами, чтобы обсудить детали. Гарантируем сокращение ре-рендеров на 30–50% и полную документацию.







