Контекст создает зависимости, а не решения. Многие React-приложения начинают страдать от проблем с производительностью, когда глобальное состояние расширяется. Особенно это заметно при использовании Context API в сочетании с useReducer
. Типичный признак: компоненты перерисовываются при любом изменении состояния, даже когда это не влияет на их отображение.
Истоки проблемы
Рассмотрим стандартную реализацию:
const AppContext = createContext();
function AppProvider({children}) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
Когда state изменяется в редюсере, все компоненты, использующие useContext(AppContext)
, перерисовываются. Почему? Потому что создается новый объект value
при каждом обновлении. React не выполняет глубокого сравнения — для него это новый контекст.
Анатомия ненужных ререндеров
Представьте авторизационный модуль со структурой:
{
user: { name: 'Alice', permissions: [...] },
theme: 'dark',
notifications: [...]
}
Компонент Header
, использующий только user.name
, будет перерисовываться при изменении количества уведомлений. В приложении среднего размера такие перерисовываются накапливаются лавинообразно.
Селекторы в контексте: эффективная альтернатива
Решение — передача не всего состояния, а селективных значений через мемоизированные контексты. Реализуем это:
import React, { createContext, useContext, useMemo } from 'react';
// Создаем контексты для каждой значимой части состояния
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationsContext = createContext();
const DispatchContext = createContext();
function AppProvider({children}) {
const [state, dispatch] = useReducer(reducer, initialState);
// Мемоизируем контекстные значения
const themeValue = useMemo(() => state.theme, [state.theme]);
const userValue = useMemo(() => state.user, [state.user]);
const notificationsValue = useMemo(() => state.notifications, [state.notifications]);
return (
<DispatchContext.Provider value={dispatch}>
<UserContext.Provider value={userValue}>
<ThemeContext.Provider value={themeValue}>
<NotificationsContext.Provider value={notificationsValue}>
{children}
</NotificationsContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
</DispatchContext.Provider>
);
}
Теперь компонент, использующий тему, не будет реагировать на изменения пользователя:
function ThemeSwitcher() {
const theme = useContext(ThemeContext);
// Перерисовывается ТОЛЬКО при изменении темы
return <div>Current theme: {theme}</div>;
}
Использование useMemo в компонентах-потребителях
Для комплексных сценариев введем пользовательский хук с селектором:
function useAppSelector(selector) {
const state = useContext(AppStateContext);
return useMemo(
() => selector(state),
[selector, state]
);
}
// Использование в компоненте
function UserProfile() {
const userName = useAppSelector(state => state.user.name);
return <div>{userName}</div>;
}
Ключевой момент: селектор должен быть стабильной ссылкой. Определяйте селекторы вне компонентов или используйте useCallback
.
// Плохо: создается при каждом рендере
const userNameSelector = (state) => state.user.name;
// Хорошо
function UserProfile() {
const userNameSelector = useCallback(state => state.user.name, []);
const userName = useAppSelector(userNameSelector);
// ...
}
Когда использовать этот подход
- Холодные данные: элементы состояния, которые изменяются независимо (темы, настройки, доменные модели)
- Часто обновляемые данные: датчики, стримы, индикаторы выполнения
- Крупные объекты: сложные вложенные структуры, где глубокое сравнение затратно
Альтернативы в экосистеме
- Redux: использует селекторы из коробки с
useSelector
, но добавляет boilerplate - Zustand: автоматическая мемоизация селекторов на уровне хранилища
- Recoil/Jotai: атомарный state-management с гранулярными обновлениями
Архитектурные рекомендации
- Дробите контексты по доменной логике: разделяйте состояние авторизации, UI-настроек, системных уведомлений
- Тестируйте ререндеры с помощью
why-did-you-render
: будет показывать компоненты, перерисовывающиеся без изменения пропсов - Используйте делегированную диспетчеризацию для сложных сценариев:
function useActions() {
const dispatch = useContext(DispatchContext);
return useMemo(() => ({
login: (creds) => dispatch({ type: 'LOGIN', payload: creds }),
logout: () => dispatch({ type: 'LOGOUT' }),
// ...
}), [dispatch]);
}
Выводы
Оптимизация контекстов в React не требует отказа от них как инструмента. Результат достигается:
- Декомпозицией одного универсального контекста на специализированные
- Селекторами с
useMemo
для управления зависимостями - Выносом примитивных значений, а не сложных объектов
Профилируйте производительность через React DevTools, чтобы убедиться, что ваши оптимизации работают. Помните: идеальное состояние приложения — когда стоимость рендеринга пропорциональна сложности UI, а не объему данных.
# Инструментарий для анализа:
npm install --save-dev @welldone-software/why-did-you-render
Инициализируйте в index.js:
import whyDidYouRender from '@welldone-software/why-did-you-render';
if (process.env.NODE_ENV !== 'production') {
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
Фокусируйтесь на точечных проблемах после выявления, вместо глобального применения сложных схем. Иногда достаточно разделить контекст пополам.