Современные React-приложения редко ограничиваются парой компонентов и тремя состояниями. Когда в игру входят авторизация, кеширование данных, мультиплатформенные настройки и сложные пользовательские workflows, управление состоянием превращается в архитектурную головоломку. Недостаточно просто бросить всё в Redux и надеяться на лучшее — каждый лишний ререндер и неоптимальная подписка на стейт могут незаметно убить производительность.
Стейт-менеджмент: не стреляйте из пушки по воробьям
Выбор инструмента часто продиктован привычкой, но реальные требования приложения должны диктовать архитектуру. Для большинства случаев достаточно комбинации:
- Локальный стейт компонента — для изолированной UI-логики (например, раскрывающееся меню)
- Context API — для глобальных но статичных значений (тема оформления, feature flags)
- Серверный стейт (React Query, SWR) — для данных из API
- Redux/Zustand — для клиентского стейта, требующего сложных синхронизаций
Ошибка №1: Засовывание серверных данных в Redux. Вместо этого используйте специализированные библиотеки:
const { data, error } = useQuery(['todos'], fetchTodos);
Эти инструменты уже дают кеширование, инвалидацию, retry-логику и синхронизацию табами браузера. Дублирование их функционала вручную — пустая трата времени.
Контролируем ререндеры: не всё, что меняется, должно триггерить обновления
Типичный сценарий проблем:
const App = () => {
const [user, setUser] = useState({ id: 1, name: 'John' });
return (
<UserContext.Provider value={{ user, setUser }}>
<Navbar />
<Content />
</UserContext.Provider>
);
};
При любом обновлении user будет ререндериться всё приложение. Решение? Разделяйте стейт и API:
const UserStateContext = createContext(null);
const UserApiContext = createContext(null);
function AppProvider({ children }) {
const [state, setState] = useState({ id: 1, name: 'John' });
const api = useMemo(() => ({
updateName: (name) => setState(prev => ({ ...prev, name })),
}), []);
return (
<UserStateContext.Provider value={state}>
<UserApiContext.Provider value={api}>
{children}
</UserApiContext.Provider>
</UserStateContext.Provider>
);
}
Теперь компоненты, вызывающие updateName
, не получат лишних ререндеров, так как api
инстанс мемоизирован.
Селекторы — ваш защитник от "цепных" обновлений
Даже с правильным разделением контекстов остаётся проблема, когда компонент использует часть большого объекта. Стандартное решение — селекторы с мемоизацией:
const selectUserPermissions = (state) => state.user.permissions;
const Navbar = () => {
const permissions = useSelector(selectUserPermissions);
// ...
};
Но в реальности этого недостаточно. Для сложных вычислений комбинируйте Reselect c кешированием:
import { createSelector } from '@reduxjs/toolkit';
const selectItems = (state) => state.store.items;
const selectSearchTerm = (state) => state.store.searchTerm;
const selectFilteredItems = createSelector(
[selectItems, selectSearchTerm],
(items, term) => items.filter(item =>
item.name.toLowerCase().includes(term.toLowerCase())
)
);
Это предотвращает пересчёт фильтрации при изменении стейта, не связанного с поиском.
Декомпозиция стейта: когда монолиту пора на операционный стол
Классическая ошибка — хранение всего приложения в едином стейт-объекте. Вместо этого применяйте domain-driven подход:
/src
/store
/auth
- slice.js
- actions.js
- selectors.js
/cart
- slice.js
- selectors.js
/modules
- rootReducer.js
- store.js
Каждая фича-слайс управляет своим участком стейта и экспортирует только селекторы и actions. Такой подход:
- Изолирует изменения в рамках домена
- Упрощает тестирование
- Позволяет ленивую загрузку стейта модуля
Асинхронные сценарии: обрабатывайте ошибки как состоявшиеся события
fetch/promise.then — прямой путь к плавающим багам. Redux Toolkit Query автоматизирует обработку асинхронных операций, но если вы пишете кастомные middleware:
const asyncMiddleware = ({ dispatch }) => next => async action => {
if (!action.payload?.promise) return next(action);
try {
const result = await action.payload.promise;
dispatch({ type: 'ASYNC_SUCCESS', payload: result });
} catch (error) {
dispatch({
type: 'ASYNC_ERROR',
payload: error,
meta: { originalAction: action }
});
// Не забудьте прокинуть ошибку дальше для обработки в UI
throw error;
}
};
Но идеальнее использовать саги (redux-saga) для сложных workflows с отменой запросов и race-условиями:
function* checkoutFlow() {
while (true) {
const { payload } = yield take('CHECKOUT_REQUEST');
const { timeout } = yield race({
checkout: call(processCheckout, payload),
cancel: take('CHECKOUT_CANCEL'),
timeout: delay(60000),
});
if (timeout) {
yield put({ type: 'CHECKOUT_TIMEOUT' });
}
}
}
Инструменты отладки: смотреть под капот без боли
React DevTools Profiler — незаменим для анализа ререндеров, но ещё несколько лайфхаков:
- Почему компонент рендерится? Используйте
whyDidYouRender
для логирования причин - Следите за стейтом Redux DevTools с time travelling для сложных сценариев
- Имитируйте проблемы искусственно замедляйте API вызовы с Chrome DevTools → Network → Throttling
Для поиска узких мест производительности собирайте метрики с помощью <React.Profiler>
, но в продакшене предпочитайте специализированные APM-инструменты.
Эволюция без переписывания: стратегия миграций
Ни один стейт-менеджмент не живёт вечно. Чтобы миграция не стала хайджекингом проекта:
- Инкапсулируйте доступ к стейту через селекторы и действия даже в Redux
- Постепенная миграция — внедряйте новый стейт-менеджер для новых фич, старый оставьте для легаси-кода
- Используйте abra-cadabra pattern — создайте прослойку, преобразующую старый стейт в новый формат
Пример миграции Redux → Zustand с сохранением обратной совместимости:
// legacy-redux-plugin.js
const useReduxStore = (selector) => {
const value = useSelector(selector);
return useSelector(selector);
};
// new-zustand-store.js
const useCombinedStore = (selector) => {
const reduxValue = useReduxStore(selector);
const zustandValue = useZustandStore(selector);
return reduxValue ?? zustandValue;
};
Управление состоянием — это баланс между предсказуемостью, производительностью и поддерживаемостью. Начните с минимализма, используйте специализированные инструменты для разных типов данных, и никогда не прекращаете профилировать. Стейт должен работать на приложение, а не приложение на стейт.