Типизация действий (actions)
На сегодняшнем занятии мы поговорим о том, как типизировать действия (actions) и состояния (state) редьюсера. Все, что мы уже успели изучить ранее, поможет вам в понимании этой темы!
Чтобы типизация действий в редьюсере не вызывала у вас вопросов, представим actions как курьеров, которые доставляют инструкции (в форме объектов) вашему useReducer. Чтобы курьеры не терялись и не путали посылки, мы будем строго их контролировать с помощью интерфейсов.
Что такое действие (action)?
Action в контексте useReducer — это объект, который описывает, что нужно сделать с состоянием. Он обычно содержит минимум одно поле type. Также он может включать дополнительные данные, которые редьюсер использует для обновления состояния.
Пример типичного действия:
const incrementAction = {
type: "INCREMENT", // Тип действия
payload: 10 // Дополнительные данные
};
Определение типов действий с TypeScript
Мы создадим интерфейсы, чтобы задать строгую структуру всех возможных действий.
Допустим, мы работаем над приложением с простым счетчиком и двумя действиями — увеличение INCREMENT и уменьшение DECREMENT значения.
Вот как выглядит типизация действий:
// Определяем интерфейсы для каждого типа действия
interface IncrementAction {
type: "INCREMENT";
payload: number;
}
interface DecrementAction {
type: "DECREMENT";
payload: number;
}
// Объединяем все возможные действия в один тип
type CounterActions = IncrementAction | DecrementAction;
Теперь CounterActions представляет одно из возможных действий. TypeScript гарантирует, что редьюсер обработает только правильные actions.
Применение типизации в редьюсере
Давайте расширим наш редьюсер, чтобы использовать типизированные действия:
interface CounterState {
count: number;
}
const counterReducer = (state: CounterState, action: CounterActions): CounterState => {
switch (action.type) {
case "INCREMENT":
return {
...state,
count: state.count + action.payload,
};
case "DECREMENT":
return {
...state,
count: state.count - action.payload,
};
default:
throw new Error("Unknown action type");
}
};
Теперь, если кто-то попытается передать action с типом "FLY_TO_THE_MOON", TypeScript бросит ошибку ещё на этапе компиляции. Никакого хаоса, только строгость.
Типизация состояния (state)
Типизация состояния в редьюсере — это ещё один важный элемент, который обеспечивает безопасность и комфорт в работе.
Как описывается состояние?
Состояние — это объект, который хранит данные, управляемые редьюсером. Оно может быть простым (например, счётчик с одним числом) или сложным (например, список задач с массивом объектов).
Простой пример:
interface CounterState {
count: number;
}
Сложный пример:
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: TodoItem[];
}
Типизация начального состояния
Начальное состояние (initial state) также должно соответствовать описанному интерфейсу:
const initialState: CounterState = {
count: 0,
};
Теперь каждый вызов редьюсера будет гарантированно работать с этим типом состояния.
Пример: управление списком задач
Давайте соберём всё вместе и создадим редьюсер для управления списком задач (todos). В нём мы будем добавлять, удалять и переключать состояние готовности задач.
Описание состояния
Сначала опишем интерфейс состояния:
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: TodoItem[];
}
Описание действий
Теперь создадим интерфейсы для действий:
interface AddTodoAction {
type: "ADD_TODO";
payload: string;
}
interface RemoveTodoAction {
type: "REMOVE_TODO";
payload: number; // id задачи
}
interface ToggleTodoAction {
type: "TOGGLE_TODO";
payload: number; // id задачи
}
type TodoActions = AddTodoAction | RemoveTodoAction | ToggleTodoAction;
Редьюсер с типизацией
Вот как будет выглядеть наш редьюсер:
const todoReducer = (state: TodoState, action: TodoActions): TodoState => {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.payload, completed: false },
],
};
case "REMOVE_TODO":
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload),
};
case "TOGGLE_TODO":
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
),
};
default:
throw new Error("Unknown action type");
}
};
Начальное состояние
И, конечно же, нам нужно задать начальное состояние:
const initialTodoState: TodoState = {
todos: [],
};
Использование редьюсера
Теперь мы можем использовать свой редьюсер в React-компоненте:
import React, { useReducer } from "react";
const TodoApp: React.FC = () => {
const [state, dispatch] = useReducer(todoReducer, initialTodoState);
const addTask = () => {
const task = prompt("Введите задачу:");
if (task) {
dispatch({ type: "ADD_TODO", payload: task });
}
};
const removeTask = (id: number) => {
dispatch({ type: "REMOVE_TODO", payload: id });
};
const toggleTask = (id: number) => {
dispatch({ type: "TOGGLE_TODO", payload: id });
};
return (
<div>
<h1>Todo List</h1>
<button onClick={addTask}>Добавить задачу</button>
<ul>
{state.todos.map(todo => (
<li key={todo.id}>
<span
style={{
textDecoration: todo.completed ? "line-through" : "none",
}}
onClick={() => toggleTask(todo.id)}
>
{todo.text}
</span>
<button onClick={() => removeTask(todo.id)}>Удалить</button>
</li>
))}
</ul>
</div>
);
};
Разбор типичных ошибок
Вот некоторые ошибки, которые могут возникнуть при типизации:
- Пропуск поля типа
typeв действиях. Помните, каждое действие должно иметь полеtype, иначе редьюсер не сможет обработать его. - Типизация состояния и действий не соответствует друг другу. Если вы добавите новое действие, не забудьте обновить тип действия и логику редьюсера.
- Необязательные поля в состоянии. Если поле может быть
undefined, явно укажите это в интерфейсе с помощью?или| undefined.
Чтобы избежать этих ошибок, старайтесь создавать интерфейсы для каждого действия и тестировать свой редьюсер.
Теперь, когда вы научились типизировать состояния и действия, ваш код стал ещё ближе к идеалу. В следующей лекции мы пойдём дальше и научимся передавать состояние через контекст с помощью useContext. Готовьтесь, дальше будет ещё интереснее!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ