useReducer + контекст
Комбинация useReducer и контекста — распространённый паттерн для управления состоянием в приложении без сторонних библиотек.
Идея
useReducer— вся логика обновления в одном редьюсере- Контекст — доступ к состоянию и
dispatchиз любого места
Редьюсер
type State = {
user: User | null;
theme: 'light' | 'dark';
};
type Action =
| { type: 'login'; user: User }
| { type: 'logout' }
| { type: 'toggleTheme' };
function appReducer(state: State, action: Action): State {
switch (action.type) {
case 'login':
return { ...state, user: action.user };
case 'logout':
return { ...state, user: null };
case 'toggleTheme':
return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
default:
return state;
}
}
Контексты
Состояние и dispatch — отдельные контексты. Компонент, которому нужен только dispatch, не перерисовывается при изменении состояния.
const AppStateContext = createContext<State | null>(null);
const AppDispatchContext = createContext<React.Dispatch<Action> | null>(null);
Провайдер
function AppProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(appReducer, {
user: null,
theme: 'light',
});
return (
<AppStateContext.Provider value={state}>
<AppDispatchContext.Provider value={dispatch}>
{children}
</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
}
Хуки
export function useAppState() {
const state = useContext(AppStateContext);
if (!state) throw new Error('useAppState must be used within AppProvider');
return state;
}
export function useAppDispatch() {
const dispatch = useContext(AppDispatchContext);
if (!dispatch) throw new Error('useAppDispatch must be used within AppProvider');
return dispatch;
}
Использование
function Header() {
const { user } = useAppState();
const dispatch = useAppDispatch();
return (
<header>
{user ? (
<>
<span>{user.name}</span>
<button onClick={() => dispatch({ type: 'logout' })}>Выйти</button>
</>
) : (
<span>Гость</span>
)}
</header>
);
}
function ThemeToggle() {
const { theme } = useAppState();
const dispatch = useAppDispatch();
return (
<button onClick={() => dispatch({ type: 'toggleTheme' })}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
Комбинация с функциями-хелперами
Чтобы не писать dispatch({ type: ... }) везде — оборачивай в функции:
function useAuthActions() {
const dispatch = useAppDispatch();
return {
login: useCallback((user: User) => dispatch({ type: 'login', user }), [dispatch]),
logout: useCallback(() => dispatch({ type: 'logout' }), [dispatch]),
};
}
Плюсы
- Нет зависимостей
- Логика централизована
- Легко тестировать редьюсер
- Разделение состояния и dispatch
Минусы
- Все потребители состояния перерисовываются при любом изменении
- Нет селекторов
- При росте проекта — boilerplate
Когда это становится проблемой — переходи на Zustand или Redux.
Итоги
useReducer+ контекст — минимальный стор- Состояние и dispatch — отдельные контексты
- Хуки с проверкой
- Функции-хелперы упрощают вызовы
- Для больших проектов — Zustand или Redux