Урок 07

Типизация контекста

Создание контекста

import { createContext } from 'react';

type AuthContextValue = {
    user: User | null;
    login: (user: User) => void;
    logout: () => void;
};

export const AuthContext = createContext<AuthContextValue | null>(null);

null — значение по умолчанию, если провайдера нет.

Провайдер

function AuthProvider({ children }: { children: ReactNode }) {
    const [user, setUser] = useState<User | null>(null);

    const value: AuthContextValue = {
        user,
        login: (u) => setUser(u),
        logout: () => setUser(null),
    };

    return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

Хук с проверкой

export function useAuth(): AuthContextValue {
    const context = useContext(AuthContext);

    if (!context) {
        throw new Error('useAuth must be used within AuthProvider');
    }

    return context;
}

Возвращаемый тип — AuthContextValue, не AuthContextValue | null. Проверка сужает.

Использование

function Profile() {
    const { user, logout } = useAuth();

    if (!user) return <p>Войдите</p>;

    return (
        <>
            <p>{user.name}</p>
            <button onClick={logout}>Выйти</button>
        </>
    );
}

TypeScript знает, что logout — функция.

Значение по умолчанию без null

Если провайдер всегда есть — можно задать заглушку:

const AuthContext = createContext<AuthContextValue>({
    user: null,
    login: () => {},
    logout: () => {},
});

Но тогда ошибка использования вне провайдера не проявится.

Generic-контекст

function createSafeContext<T>(name: string) {
    const Context = createContext<T | null>(null);

    function useSafeContext(): T {
        const context = useContext(Context);
        if (!context) {
            throw new Error(`${name} must be used within provider`);
        }
        return context;
    }

    return [Context.Provider, useSafeContext] as const;
}

const [AuthProvider, useAuth] = createSafeContext<AuthContextValue>('Auth');

Разделение контекстов

Данные и действия — отдельные контексты:

const UserContext = createContext<User | null>(null);
const UserActionsContext = createContext<UserActions | null>(null);

Хуки:

function useUser(): User | null {
    return useContext(UserContext);
}

function useUserActions(): UserActions {
    const context = useContext(UserActionsContext);
    if (!context) throw new Error('UserActionsContext is missing');
    return context;
}

Значение с useMemo

const value = useMemo<AuthContextValue>(
    () => ({ user, login, logout }),
    [user, login, logout]
);

Контекст с reducer

type State = { count: number };
type Action = { type: 'increment' } | { type: 'decrement' };

const StateContext = createContext<State | null>(null);
const DispatchContext = createContext<Dispatch<Action> | null>(null);

function Provider({ children }: { children: ReactNode }) {
    const [state, dispatch] = useReducer(reducer, { count: 0 });

    return (
        <StateContext.Provider value={state}>
            <DispatchContext.Provider value={dispatch}>
                {children}
            </DispatchContext.Provider>
        </StateContext.Provider>
    );
}

Dispatch<Action> — встроенный тип React.

Итоги

  • createContext<T | null>(null) для проверки
  • Хук с проверкой возвращает T
  • Generic-фабрика createSafeContext
  • Разделяй данные и действия
  • Dispatch<Action> для reducer