Локальное состояние против глобального и цена Context
Вопрос, отделяющий «читал документацию» от «делал приложение»: почему Context не заменяет state manager.
Context — способ доставить значение вниз по дереву, минуя props. Это транспорт, а не хранилище: у него нет ни точечных подписок, ни встроенной оптимизации обновлений.
Вопрос
«Как решаете, где хранить состояние? Почему нельзя просто положить всё в Context вместо Redux?»
Что на самом деле проверяет интервьюер
Есть ли у вас критерии выбора. Плохой ответ — «беру Redux, потому что так принято» или «Context проще, зачем библиотеки». Хороший — разложить состояние на виды и объяснить, что и почему живёт локально, а что глобально.
Развёрнутый ответ
Четыре вида состояния
- Локальное UI-состояние: открыт ли аккордеон, введённый текст, выбранная вкладка. Живёт в
useStateвнутри компонента. Выносить его наверх не нужно. - Разделяемое состояние: текущий пользователь, тема, корзина, язык. Нужно многим экранам — вот здесь появляется контекст или store.
- Серверное состояние: данные из API. У него свои проблемы — кэш, устаревание, повторные запросы, — поэтому его обычно отдают TanStack Query или RTK Query, а не хранят руками.
- Состояние навигации: где находится пользователь. Этим управляет React Navigation, дублировать его не нужно.
Правило по умолчанию: поднимайте состояние ровно до того компонента, где оно реально нужно двум и более потребителям, и ни уровнем выше.
Цена Context
Все компоненты, вызывающие useContext, перерисовываются при любом изменении значения контекста — независимо от того, какую часть данных они используют. Сравнение идёт по ссылке.
// ПЛОХО: новый объект на каждом рендере провайдера
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
Даже если user не изменился, любой рендер провайдера (например, из-за другого состояния рядом) создаст новый объект и разбудит всех потребителей. Первое лекарство — useMemo:
const value = useMemo(() => ({ user, login, logout }), [user, login, logout]);
Второе, более сильное, — разделить контексты на «редко меняющиеся действия» и «часто меняющиеся данные»:
const AuthActionsContext = createContext(null); // login, logout — стабильны
const AuthStateContext = createContext(null); // user — меняется
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const actions = useMemo(() => ({
login: async (creds) => setUser(await api.login(creds)),
logout: () => setUser(null),
}), []);
return (
<AuthActionsContext.Provider value={actions}>
<AuthStateContext.Provider value={user}>{children}</AuthStateContext.Provider>
</AuthActionsContext.Provider>
);
}
Теперь кнопка «Выйти», которой нужны только действия, не перерисовывается при обновлении профиля.
Почему на мобильном это важнее, чем в вебе
Лавина рендеров в браузере на десктопе часто незаметна. На бюджетном Android она даёт пропущенные кадры при скролле и ощутимую задержку отклика. Кроме того, JS-поток в RN один: лишняя работа в нём напрямую отнимает время у обработки жестов.
Когда Context достаточен
- Тема оформления, локаль, флаги фич — меняются редко.
- Небольшое приложение с одним-двумя разделяемыми значениями.
- Инъекция зависимостей: клиент API, конфигурация, аналитика.
Когда пора брать store: много несвязанных частей состояния, нужны точечные подписки на отдельные поля, требуется middleware (логирование, persist, offline-очередь), важна отладка через инструменты и предсказуемая история изменений.
Типичные ошибки кандидатов
- Говорят «Context — это встроенный Redux». Разные задачи: Redux хранит и позволяет подписаться на срез, Context только доставляет значение.
- Складывают в один провайдер всё приложение и получают перерисовку всего дерева на каждый чих.
- Держат серверные данные в глобальном store вручную, изобретая кэш и инвалидацию, вместо готового решения.
- Поднимают наверх локальное состояние формы — потом каждый символ в поле ввода перерисовывает половину экрана.
Как ответить кратко
«Разделяю состояние на локальное UI, разделяемое клиентское, серверное и состояние навигации. Локальное держу в useState рядом с компонентом, серверное отдаю TanStack Query или RTK Query — там кэш и инвалидация из коробки. Context — это транспорт, а не хранилище: он доставляет значение вниз, но перерисовывает всех потребителей при любом изменении значения, потому что сравнение идёт по ссылке. Поэтому значение мемоизирую и разделяю на контекст действий и контекст данных. Когда частей состояния много и нужны точечные подписки, middleware и отладка — беру полноценный store.»