Типизация контекста: основы
Контекст — это отличный инструмент для управления состоянием приложения. Но как только в вашем приложении появляется сложная структура данных, ошибки в передаваемых значениях могут становиться кошмаром для отладки. TypeScript позволяет нам гарантировать, что значения в контексте имеют ожидаемые типы. Это особенно полезно, когда контекст распространяется по всему приложению, и вы хотите быть уверены, что ваши компоненты получают только те данные, которые ожидаются.
Давайте начнём с простого примера и планомерно усложним его.
Создание интерфейса для контекста
Допустим, мы работаем с простым состоянием пользователя, в котором хранятся его имя и информацию об авторизации. Сначала определим интерфейс.
interface UserContextType {
name: string;
isLoggedIn: boolean;
}
Теперь, создавая контекст с этим интерфейсом, мы можем использовать React.createContext:
import React, { createContext } from 'react';
// Объявляем дефолтное значение контекста
const defaultUserContext: UserContextType = {
name: '',
isLoggedIn: false,
};
// Создаём сам контекст
const UserContext = createContext<UserContextType>(defaultUserContext);
export default UserContext;
В этом примере мы сразу определили интерфейс UserContextType, который будет использоваться для типизации данных в контексте. Это простой, но мощный способ избежать ошибок, поскольку в дальнейшем TypeScript будет проверять любые данные, которые мы пытаемся передать в контекст.
Типизация значений, передаваемых через контекст
Использование контекста в компонентах
Теперь давайте используем наш контекст в компоненте. Для этого воспользуемся хуком useContext.
import React, { useContext } from 'react';
import UserContext from './UserContext';
const UserProfile: React.FC = () => {
// Получаем данные из контекста
const { name, isLoggedIn } = useContext(UserContext);
return (
<div>
<h1>Профиль пользователя</h1>
{isLoggedIn ? <p>Добро пожаловать, {name}!</p> : <p>Вы не авторизованы.</p>}
</div>
);
};
export default UserProfile;
Отлично! Теперь, если мы забудем передать имя или значение isLoggedIn в поставщике контекста (Context Provider), TypeScript моментально предупредит нас об ошибке.
Типизация контекста с помощью дополнительных интерфейсов
Добавление методов в контекст
Часто контекст передаёт не просто данные, но и функции для их обновления. Давайте добавим метод для логина пользователя.
interface UserContextType {
name: string;
isLoggedIn: boolean;
login: (name: string) => void;
}
// Дефолтное значение должно включать и метод login
const defaultUserContext: UserContextType = {
name: '',
isLoggedIn: false,
login: () => {}, // Заглушка по умолчанию
};
const UserContext = createContext<UserContextType>(defaultUserContext);
Реализация контекста с провайдером
Давайте теперь расширим наш контекст, добавив провайдер, который будет управлять состоянием.
import React, { useState, FC } from 'react';
import UserContext from './UserContext';
const UserProvider: FC = ({ children }) => {
const [name, setName] = useState('');
const [isLoggedIn, setIsLoggedIn] = useState(false);
// Метод для логина
const login = (userName: string) => {
setName(userName);
setIsLoggedIn(true);
};
// Передаём состояние и метод в провайдер
return (
<UserContext.Provider value={{ name, isLoggedIn, login }}>
{children}
</UserContext.Provider>
);
};
export default UserProvider;
Теперь мы можем завернуть все компоненты нашего приложения в UserProvider, чтобы данные контекста name, isLoggedIn, login были доступны везде.
Типизация контекста с использованием useReducer
Для более сложного состояния можно использовать useReducer. В таком случае типизация становится немного сложнее, но от этого интереснее.
Интерфейсы для состояния и действий
interface UserState {
name: string;
isLoggedIn: boolean;
}
type UserAction =
| { type: 'LOGIN'; payload: string }
| { type: 'LOGOUT' };
Редьюсер и контекст
Определим редьюсер для управления состоянием:
const userReducer = (state: UserState, action: UserAction): UserState => {
switch (action.type) {
case 'LOGIN':
return { name: action.payload, isLoggedIn: true };
case 'LOGOUT':
return { name: '', isLoggedIn: false };
default:
return state;
}
};
Теперь создадим наш контекст:
interface UserContextType {
state: UserState;
dispatch: React.Dispatch<UserAction<;
}
const defaultUserContext: UserContextType = {
state: { name: '', isLoggedIn: false },
dispatch: () => {}, // Заглушка по умолчанию
};
const UserContext = createContext<UserContextType<(defaultUserContext);
Провайдер для контекста
Как и раньше, мы должны создать провайдер, который передаёт данные из редьюсера в контекст.
const UserProvider: FC = ({ children }) => {
const [state, dispatch] = React.useReducer(userReducer, defaultUserContext.state);
return (
<UserContext.Provider value={{ state, dispatch }}>
{children}
</UserContext.Provider>
);
};
export default UserProvider;
Теперь состояние state и функция dispatch доступны в любом компоненте через useContext.
Практическое применение: работа с типизированным контекстом
Допустим, нам нужно создать компонент, который позволяет пользователю логиниться и выходить из приложения.
import React, { useContext } from 'react';
import UserContext from './UserContext';
const LoginButton: React.FC = () => {
const { state, dispatch } = useContext(UserContext);
const handleLogin = () => {
const userName = prompt('Введите ваше имя:') || '';
dispatch({ type: 'LOGIN', payload: userName });
};
const handleLogout = () => {
dispatch({ type: 'LOGOUT' });
};
return (
<div>
{state.isLoggedIn ? (
<>
<p>Привет, {state.name}!</p>
<button onClick={handleLogout}>Выйти</button>
</>
) : (
<button onClick={handleLogin}>Войти</button>
)}
</div>
);
};
export default LoginButton;
Готово! Мы создали полностью типизированный контекст, интегрировали его с useReducer и обеспечили корректную передачу данных.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ