Пошук уроків, статей та іншого контенту
Навчитеся описувати значення React Context, його провайдер і безпечне використання в компонентах.
Context дає змогу передавати значення через дерево компонентів без прокидання props на кожному рівні. Але без типів легко:
передати в Provider неповне або неправильне значення;
звернутися до властивості, якої немає в контексті;
використати контекст поза його Provider;
отримати помилку через undefined під час виконання.
Типізація має описувати:
форму значення контексту;
пропси компонента-провайдера;
результат використання контексту в компонентах.
Для початку створимо тип користувача та тип усього значення контексту:
type User = {
id: number;
name: string;
};
type AuthContextValue = {
user: User | null;
login: (name: string) => void;
logout: () => void;
};Властивість user може бути null, якщо користувач ще не увійшов у систему.
Функції також потрібно типізувати. У цьому прикладі вони не приймають і не повертають значення, окрім login, яка отримує ім’я користувача.
createContext очікує початкове значення. Якщо контекст обов’язково має використовуватися всередині Provider, безпечним варіантом буде undefined:
import { createContext } from "react";
const AuthContext = createContext<AuthContextValue | undefined>(undefined);Тепер TypeScript знає, що контекст або містить AuthContextValue, або дорівнює undefined.
Це змушує нас обробити випадок, коли компонент намагається використати контекст без відповідного провайдера.
Компонент-провайдер зазвичай отримує children. Для типізації children використаємо ReactNode:
import type { ReactNode } from "react";
type AuthProviderProps = {
children: ReactNode;
};ReactNode охоплює JSX-елементи, текст, фрагменти, масиви елементів і null.
Реалізація провайдера може виглядати так:
import { useState } from "react";
function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const login = (name: string) => {
setUser({
id: 1,
name,
});
};
const logout = () => {
setUser(null);
};
const value: AuthContextValue = {
user,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}Тип AuthContextValue для змінної value перевіряє, що провайдер передає всі необхідні властивості правильних типів.
Наприклад, TypeScript повідомить про помилку, якщо:
не додати logout;
передати user неправильного типу;
зробити login без параметра name;
повернути з функції значення, якого не передбачає тип.
Безпосереднє використання useContext повертає значення типу:
AuthContextValue | undefinedТому в кожному компоненті довелося б перевіряти undefined. Замість цього створимо власний хук:
import { useContext } from "react";
function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth має використовуватися всередині AuthProvider");
}
return context;
}Після перевірки TypeScript розуміє, що context має тип AuthContextValue. Компоненти, які викликають useAuth, також отримують уже звужений тип без undefined.
Нижче наведено самодостатній приклад для файлу main.tsx у React-проєкті з TypeScript:
import { StrictMode, createContext, useContext, useState } from "react";
import type { ReactNode } from "react";
import { createRoot } from "react-dom/client";
type User = {
id: number;
name: string;
};
type AuthContextValue = {
user: User | null;
login: (name: string) => void;
logout: () => void;
};
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
type AuthProviderProps = {
children: ReactNode;
};
function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const login = (name: string) => {
setUser({
id: 1,
name,
});
};
const logout = () => {
setUser(null);
};
const value: AuthContextValue = {
user,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth має використовуватися всередині AuthProvider");
}
return context;
}
function UserPanel() {
const { user, login, logout } = useAuth();
if (user === null) {
return (
<section>
<p>Користувач не авторизований.</p>
<button type="button" onClick={() => login("Олена")}>
Увійти
</button>
</section>
);
}
return (
<section>
<p>Вітаємо, {user.name}!</p>
<button type="button" onClick={logout}>
Вийти
</button>
</section>
);
}
function App() {
return (
<AuthProvider>
<main>
<h1>Профіль</h1>
<UserPanel />
</main>
</AuthProvider>
);
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);У компоненті UserPanel TypeScript знає:
user має тип User | null;
login приймає рядок;
logout не приймає аргументів;
помилка з undefined неможлива, якщо компонент правильно використовує useAuth.
Іноді контекст може мати повноцінне значення за замовчуванням. Наприклад, для теми можна створити контекст так:
type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Theme;
toggleTheme: () => void;
};
const defaultThemeContext: ThemeContextValue = {
theme: "light",
toggleTheme: () => {
// Функція за замовчуванням нічого не робить
},
};
const ThemeContext = createContext<ThemeContextValue>(defaultThemeContext);У такому випадку useContext(ThemeContext) завжди повертає ThemeContextValue, а не undefined.
Такий підхід доречний, якщо робоче значення справді має сенс без Provider. Якщо ж Provider обов’язковий, краще використовувати undefined і власний хук із перевіркою.
Можна передати об’єкт безпосередньо в Provider:
<AuthContext.Provider
value={{
user,
login,
logout,
}}
>
{children}
</AuthContext.Provider>TypeScript також перевірить це значення відповідно до типу, переданого в createContext.
Окрема змінна value часто робить код зрозумілішим і спрощує додавання інших властивостей. Вона також зручна, якщо значення потрібно підготувати перед передаванням у Provider.
asНевдалий варіант:
const AuthContext = createContext({} as AuthContextValue);Такий запис приховує проблему від TypeScript. Насправді об’єкт {} не містить user, login і logout, але компілятор змушений повірити в це приведення типу.
Краще явно вказати undefined:
const AuthContext = createContext<AuthContextValue | undefined>(undefined);undefinedЯкщо Context оголошено так:
const AuthContext = createContext<AuthContextValue | undefined>(undefined);то цей код потенційно небезпечний:
function UserPanel() {
const context = useContext(AuthContext);
return <p>{context.user?.name}</p>;
}context може бути undefined, тому звернення до context.user спричинить помилку.
Потрібно або виконати перевірку безпосередньо:
function UserPanel() {
const context = useContext(AuthContext);
if (context === undefined) {
return <p>Контекст недоступний.</p>;
}
return <p>{context.user?.name}</p>;
}або використовувати власний хук useAuth.
childrenНе варто типізувати children лише як JSX.Element, якщо провайдер може отримати null, текст або кілька елементів:
type IncorrectProps = {
children: JSX.Element;
};Зазвичай правильніший варіант:
import type { ReactNode } from "react";
type AuthProviderProps = {
children: ReactNode;
};Якщо тип передбачає функцію:
type AuthContextValue = {
logout: () => void;
};то Provider має передати саме функцію:
const value: AuthContextValue = {
logout: () => {
setUser(null);
},
};Рядок, об’єкт або функція з несумісними параметрами призведуть до помилки TypeScript.
Для невеликого контексту всі типи та реалізацію можна зберігати в одному файлі. Для більших частин застосунку зручно розділити їх:
AuthContext.tsx — типи, контекст, AuthProvider і useAuth;
компоненти — лише використання useAuth.
Компоненти при цьому не повинні напряму працювати з неперевіреним useContext. Власний хук приховує технічну перевірку та надає компонентам безпечний типізований API.
Тип контексту описує всі властивості та функції, які передаються через Provider.
Для обов’язкового Provider використовуйте createContext<Type | undefined>(undefined).
Проп children зазвичай типізується як ReactNode.
Власний хук може перевірити наявність Provider і повернути значення без undefined.
Не маскуйте відсутність початкового значення через as.
Значення за замовчуванням без undefined підходить лише тоді, коли контекст справді може працювати без провайдера.