На чем лучше построить изменение темы: useContext или Redux
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
useContext vs Redux для управления темой
Это частый вопрос при выборе архитектуры для global state. Оба подхода работают, но каждый имеет свои плюсы и минусы. Разберёмся, когда выбирать что.
useContext + useState: простое решение
// createThemeContext.js
import { createContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
// Использование
function Button() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Текущая тема: {theme}
</button>
);
}
// В App.js
function App() {
return (
<ThemeProvider>
<Button />
</ThemeProvider>
);
}
Redux: более сложное решение
// themeSlice.js
import { createSlice } from '@reduxjs/toolkit';
const themeSlice = createSlice({
name: 'theme',
initialState: { theme: 'light' },
reducers: {
toggleTheme: (state) => {
state.theme = state.theme === 'light' ? 'dark' : 'light';
},
setTheme: (state, action) => {
state.theme = action.payload;
}
}
});
export const { toggleTheme, setTheme } = themeSlice.actions;
export default themeSlice.reducer;
// store.js
import { configureStore } from '@reduxjs/toolkit';
import themeReducer from './themeSlice';
const store = configureStore({
reducer: {
theme: themeReducer
}
});
export default store;
// Использование
function Button() {
const theme = useSelector(state => state.theme.theme);
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(toggleTheme())}>
Текущая тема: {theme}
</button>
);
}
// В main.js
function App() {
return (
<Provider store={store}>
<Button />
</Provider>
);
}
Сравнение: теория
useContext:
- Встроено в React, без доп. библиотек
- Может привести к ненужным ре-рендерам
- Простая кривая обучения
- Подходит для простых данных
Redux:
- Мощный и масштабируемый
- Предсказуемый и отлаживаемый (DevTools)
- Требует много boilerplate кода
- Идеален для сложного state
Проблемы useContext с темой
Проблема 1: Ненужные ре-рендеры
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null); // Другое состояние
const value = { theme, setTheme, user, setUser };
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// Любой компонент, который использует ThemeContext
function Button() {
const { theme } = useContext(ThemeContext);
// Когда меняется user — Button ре-рендерится, хотя theme не изменилась!
return <button>{theme}</button>;
}
Решение 1: Разделить контексты
const ThemeContext = createContext();
const UserContext = createContext();
function App() {
return (
<ThemeProvider>
<UserProvider>
<Button /> // Теперь изолировано
</UserProvider>
</ThemeProvider>
);
}
Решение 2: useCallback + useMemo
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null);
// Memo для theme отдельно
const themeValue = useMemo(() => ({
theme,
setTheme
}), [theme]);
const userValue = useMemo(() => ({
user,
setUser
}), [user]);
return (
<ThemeContext.Provider value={themeValue}>
<UserContext.Provider value={userValue}>
{children}
</UserContext.Provider>
</ThemeContext.Provider>
);
}
Проблема 2: DevTools и debugging
Redux DevTools показывает все действия и изменения state. useContext не предоставляет такой информации:
// Redux DevTools
// Action: THEME/toggleTheme
// Old State: { theme: 'light' }
// New State: { theme: 'dark' }
// Difference: { theme changed from light to dark }
// useContext
// Просто re-render, сложно отследить что изменилось
Когда использовать useContext
useContext идеален для:
- Простая тема (только 2-3 состояния)
function ThemeProvider({ children }) {
const [isDark, setIsDark] = useState(false);
return (
<ThemeContext.Provider value={{ isDark, setIsDark }}>
{children}
</ThemeContext.Provider>
);
}
// Достаточно простой
-
Небольшой проект (5-10 компонентов)
-
Не нужны DevTools и time-travel debugging
-
Нет сложного state management
// Например, просто проксирование одного значения
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState(
localStorage.getItem('theme') || 'light'
);
const toggleTheme = (newTheme) => {
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
Когда использовать Redux
Redux идеален для:
-
Большой проект (100+ компонентов)
-
Сложная логика темы (переходы, анимации, системная тема)
const themeSlice = createSlice({
name: 'theme',
initialState: {
theme: localStorage.getItem('theme') || 'light',
transition: false,
systemPreference: window.matchMedia('(prefers-color-scheme: dark)').matches
},
reducers: {
setTheme: (state, action) => {
state.theme = action.payload;
state.transition = true;
},
syncSystemTheme: (state) => {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
state.theme = isDark ? 'dark' : 'light';
}
}
});
-
Нужны DevTools, отладка, история действий
-
Много других глобальных состояний (auth, notifications, filters)
// Одного Redux store хватает для всего
const store = configureStore({
reducer: {
theme: themeReducer,
auth: authReducer,
notifications: notificationsReducer,
ui: uiReducer
}
});
- Нужна предсказуемость (тестирование, воспроизведение ошибок)
Рекомендация
Таблица принятия решения:
Особенность | useContext | Redux
----------------------------------------------------
Простая тема | ✓ ✓ ✓ | OK
Большой проект | ? ? | ✓ ✓ ✓
Много состояния | OK | ✓ ✓ ✓
DevTools нужны | No | ✓ ✓ ✓
Быстро настроить | ✓ ✓ ✓ | No
Мало зависимостей | ✓ ✓ ✓ | No
Отладка сложная | Hard | ✓ ✓ ✓
Тестирование | OK | ✓ ✓ ✓
Практическая рекомендация
Начни с useContext:
// Просто, работает, хватает для большинства проектов
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
localStorage.setItem('theme', theme);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
Если возникнут проблемы с производительностью или отладкой — переходи на Redux.
Или используй Zustand (middle ground):
// Меньше boilerplate чем Redux, больше мощи чем useContext
import { create } from 'zustand';
const useTheme = create((set) => ({
theme: 'light',
toggleTheme: () => set(state => ({
theme: state.theme === 'light' ? 'dark' : 'light'
}))
}));
function Button() {
const { theme, toggleTheme } = useTheme();
return <button onClick={toggleTheme}>{theme}</button>;
}
Заключение
Для управления темой:
- Маленький проект → useContext
- Большой проект или много state → Redux
- Хочешь middle ground → Zustand
Совет: Не переусложняй. useContext + localStorage часто достаточно для управления темой. Используй Redux, только если действительно нужна его мощь.