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% и полную документацию.







