Урок 08

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