Як типізувати контекст?
1) Базовий і безпечний патерн (з undefined + кастомний хук)
Коли контекст обов'язковий усередині піддерева і не повинен бути null/undefined під час використання.
javascript
import React, { createContext, useContext, ReactNode } from "react";
type Theme = "light" | "dark";
type ThemeCtx = {
theme: Theme;
setTheme: (t: Theme) => void;
};
// 1) Контекст допускає undefined до Provider
const ThemeContext = createContext<ThemeCtx | undefined>(undefined);
// 2) Безпечний хук: кидає зрозумілу помилку, якщо провайдера немає
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within <ThemeProvider>");
return ctx;
}
// 3) Провайдер із типами пропсів
export function ThemeProvider({
value,
children,
}: {
value: ThemeCtx;
children: ReactNode;
}) {
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}Чому так краще:
- не потрібно писати
!/кастуватиas ThemeCtx; - отримуємо ранню і зрозумілу помилку, якщо забули обгорнути в провайдер.
2) Контекст із валідним дефолтом (якщо він реально є)
Якщо поза провайдером значення осмислене (наприклад, read-only налаштування).
javascript
type Config = { apiBase: string; locale: string };
const defaultConfig: Config = { apiBase: "/api", locale: "en" };
const ConfigContext = createContext<Config>(defaultConfig);
export const useConfig = () => useContext(ConfigContext);Важливо: давай дефолт тільки тоді, коли він справді коректний для runtime, інакше ти приховаєш баг.
3) Контекст, який може бути null за доменом
Якщо «немає значення» - це нормальний кейс.
javascript
type User = { id: string; name: string } | null;
const CurrentUserContext = createContext<User>(null);
export const useCurrentUser = () => useContext(CurrentUserContext);4) Пара «state + dispatch» (зручно для редьюсерів)
javascript
type Action =
| { type: "set"; payload: number }
| { type: "reset" };
type CounterState = { count: number };
type CounterCtx = {
state: CounterState;
dispatch: React.Dispatch<Action>;
};
const CounterContext = createContext<CounterCtx | undefined>(undefined);
export function useCounter() {
const ctx = useContext(CounterContext);
if (!ctx) throw new Error("useCounter must be used within <CounterProvider>");
return ctx;
}5) Кортеж/мульти-значення (іноді зручніше)
javascript
type Filters = { q: string; tags: string[] };
type SetFilters = (next: Filters) => void;
const FiltersContext = createContext<[Filters, SetFilters] | undefined>(undefined);
export function useFilters() {
const ctx = useContext(FiltersContext);
if (!ctx) throw new Error("useFilters must be used within <FiltersProvider>");
return ctx; // поверне [filters, setFilters]
}6) Типізація провайдера (пропси value і children)
javascript
type AuthCtx = { token: string | null; login: (t: string) => void };
const AuthContext = createContext<AuthCtx | undefined>(undefined);
type AuthProviderProps = {
value: AuthCtx; // строго типізуємо value
children: ReactNode;
};
export function AuthProvider({ value, children }: AuthProviderProps) {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}7) Уникай анти-патернів
createContext<Ctx>(null as unknown as Ctx)чиnull!- приховує помилки.- Давати «фіктивний» дефолт, якщо без провайдера працювати не можна.
- Зберігати в контексті часто змінювані примітиви без потреби (спричинить зайві рендери).
8) Нюанси і поради
-
Для великих значень мемоізуй
valueу провайдері:javascriptconst value = useMemo(() => ({ theme, setTheme }), [theme]); -
Для SSR/тестів зручно мати валідний дефолт (патерн №2) - якщо це справді безпечно.
-
Якщо контекст використовується в різних пакетах, експортуй і контекст, і хук, але споживачам віддавай саме хук - так ти нав'яжеш безпечне використання.
Коротка шпаргалка
- Обов'язковий провайдер ->
createContext<T | undefined>(undefined)+useXxx()з помилкою. - Є осмислений дефолт ->
createContext<T>(defaultValue). - Допускається відсутність ->
createContext<T | null>(null). - Reducer-стиль ->
createContext<{ state: S; dispatch: Dispatch<A> } | undefined>(undefined).
Коротка відповідь
Для співбесідиPremium
Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.