Распространённая картина: приложение на React начинает тормозить, интерфейс дёргается при обновлении состояния, а в инструментах разработчика мелькают десятки ненужных перерисовок. Часто корень проблемы — в неоптимальном управлении состоянием. Сегодня разберём как избежать этих проблем, работая с Context API и Redux Toolkit.
Анатомия проблемы: почему ваш Context перерисовывает всё подряд
Рассмотрим типичный пример провайдера авторизации:
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [permissions, setPermissions] = useState([]);
const login = async (credentials) => {
setIsLoading(true);
const data = await api.login(credentials);
setUser(data.user);
setPermissions(data.permissions);
setIsLoading(false);
};
const value = { user, isLoading, permissions, login };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
Почему это проблематично? При любом изменении состояния — обновлении isLoading
во время логина — все компоненты, использующие этот контекст, перерисуются, даже если им нужна только часть данных. Компоненту отображения пермишенов не важно состояние загрузки, но он всё равно получит ненужный ре-рендер.
Диагностика в DevTools
Откройте React DevTools, включите "Highlight updates when components render". Вы увидите, как при изменении isLoading
подсвечивается всё дерево ниже провайдера — ключевой маркер избыточных ре-рендеров.
Решения для Context: разделяй и властвуй
Стратегия 1: Мемоизация контекста
export function AuthProvider({ children }) {
// Состояние остаётся прежним...
const login = useCallback(async (credentials) => {
// Логика входа
}, []);
const value = useMemo(() => ({
user,
isLoading,
permissions,
login
}), [user, isLoading, permissions, login]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
useMemo
и useCallback
предотвращают создание нового объекта значения при каждом рендере. Но проблема избыточных перерисовок остаётся при обновлении любых данных.
Стратегия 2: Сегментирование контекстов
Создадим независимые контексты для разных логических блоков:
const UserContext = createContext();
const LoadingContext = createContext();
const PermissionsContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [permissions, setPermissions] = useState([]);
return (
<LoadingContext.Provider value={isLoading}>
<UserContext.Provider value={user}>
<PermissionsContext.Provider value={permissions}>
{children}
</PermissionsContext.Provider>
</UserContext.Provider>
</LoadingContext.Provider>
);
}
Теперь компонент, использующий только пермишены, не будет реагировать на изменения isLoading
или user
.
Когда Context становится недостаточным
Для простых приложений этих оптимизаций достаточно. Но при масштабировании возникают новые проблемы:
- Обновления, зависящие от нескольких состояний: Компоненту нужны данные из двух несвязанных контекстов
- Асинхронные зависимости: Загрузка данных с последовательными запросами
- Сложные преобразования данных: Комбинирование данных из нескольких источников
- DevTools и история изменений: Отладка потока данных
Рассмотрим запрос, где нам нужны данные пользователя и его настроек:
const { user } = useContext(UserContext);
const { preferences } = useContext(PreferencesContext);
useEffect(() => {
if (user && preferences) {
loadDashboardData(user.id, preferences.locale);
}
}, [user, preferences]); // Сложная зависимость
Эффект запускается хаотично при независимых обновлениях контекстов. Войдите в Redux Toolkit.
Redux Toolkit: не ваш дедушкин Redux
Редокс избавляется от шаблонного кода через:
- createSlice: Объединяет экшены и редьюсеры
- createAsyncThunk: Стандартизация асинхронных операций
- RTK Query: Встроенное решение для API-запросов
- Оптимизированные селекторы: createSelector из Reselect
Реализация нашего сценария с RTK
// store/authSlice.js
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
permissions: [],
isLoading: false
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(loginUser.pending, (state) => {
state.isLoading = true;
});
builder.addCase(loginUser.fulfilled, (state, action) => {
state.user = action.payload.user;
state.permissions = action.payload.permissions;
state.isLoading = false;
});
}
});
export const loginUser = createAsyncThunk(
'auth/login',
async (credentials, { rejectWithValue }) => {
try {
return await api.login(credentials);
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
Селекторы с мемоизацией
const selectUser = state => state.auth.user;
const selectPermissions = state => state.auth.permissions;
export const selectUserPermissions = createSelector(
[selectUser, selectPermissions],
(user, permissions) => {
// Тяжёлая логика преобразования
return permissions.map(perm => `${user.id}_${perm}`);
}
);
Почему это эффективнее: преобразование выполняется только при изменении исходных user
или permissions
.
Интеграция с React
const Dashboard = () => {
const userPermissions = useSelector(selectUserPermissions);
const isLoading = useSelector(state => state.auth.isLoading);
if (isLoading) return <Spinner />;
return <PermissionsList items={userPermissions} />;
}
Redux автоматически обрабатывает подписки и предотвращает ре-рендеры когда данные не изменились.
Контрольный список: Context или Redux Toolkit?
Критерий | Context API | Redux Toolkit |
---|---|---|
Размер приложения | Малый/Средний | Средний/Крупный |
Частота обновлений состояния | Низкая/Средняя | Высокая |
Сложность преобразования данных | Простые операции | Комплексные селекторы |
Асинхронные потоки данных | Ручное управление | Встроенная поддержка |
Инструменты отладки | Базовые | DevTools + Time Travel |
Bundle size | 0Kb (встроен в React) | ≈10Kb (gzipped) |
Гибридный подход: лучшее из двух миров
Нет правил без исключений. Мы часто комбинируем подходы:
// Структура проекта:
/src
/store # Redux для глобального состояния
/context # Локальные контексты для UI состояний
Пример: используем Redux для данных пользователя и API-запросов, а Context для темы оформления и локальных состояний формы:
const ThemeContext = createContext();
export function App() {
return (
<ReduxProvider store={store}>
<ThemeContext.Provider value={darkMode}>
<MainLayout />
</ThemeContext.Provider>
</ReduxProvider>
);
}
// Где-то внутри компонента:
const theme = useContext(ThemeContext);
const currentUser = useSelector(selectCurrentUser);
Микроменеджмент подписок
Для максимальной производительности в сложных списках соединяем React.memo с селекторами:
const UserListItem = memo(({ userId }) => {
const user = useSelector(state =>
selectUserById(state, userId) // Селектор с кешированием
);
return <li>{user.name}</li>;
});
Заключение: рекомендации для реальных проектов
-
Начинайте с Context: Для простых сценариев используйте разделённые контексты с
useMemo
/useCallback
-
Переходите на Redux Toolkit при:
- Появлении компонентов, зависящих от нескольких состояний
- Необходимости сложных преобразований данных
- Потребности в продвинутой отладке
-
Оптимизируйте селекторы: Всегда используйте memoized селекторы при работе с Redux
-
Измеряйте производительность: Не гадайте — используйте React DevTools Profiler и
window.performance.mark()
-
Избегайте преждевременной оптимизации: Добавляйте сложные решения только при явных метриках проблем
Оптимальное управление состоянием в React напоминает дирижирование оркестром: каждая часть должна обновляться в нужный момент, без лишних партий. Выбор инструментов должен определяться спецификой вашего приложения, а не модными трендами. Когда в следующий раз увидите дёргающийся интерфейс, вспомните — вероятно, где-то кто-то передаёт объект с зависимостями без мемоизации. Не будьте этим человеком.