JavaRush /Курсы /Модуль 3: React /Типизация действий (actions) и состояний в редьюсере

Типизация действий (actions) и состояний в редьюсере

Модуль 3: React
2 уровень , 3 лекция
Открыта

Типизация действий (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>
  );
};

Разбор типичных ошибок

Вот некоторые ошибки, которые могут возникнуть при типизации:

  1. Пропуск поля типа type в действиях. Помните, каждое действие должно иметь поле type, иначе редьюсер не сможет обработать его.
  2. Типизация состояния и действий не соответствует друг другу. Если вы добавите новое действие, не забудьте обновить тип действия и логику редьюсера.
  3. Необязательные поля в состоянии. Если поле может быть undefined, явно укажите это в интерфейсе с помощью ? или | undefined.

Чтобы избежать этих ошибок, старайтесь создавать интерфейсы для каждого действия и тестировать свой редьюсер.

Теперь, когда вы научились типизировать состояния и действия, ваш код стал ещё ближе к идеалу. В следующей лекции мы пойдём дальше и научимся передавать состояние через контекст с помощью useContext. Готовьтесь, дальше будет ещё интереснее!

1
Задача
Модуль 3: React, 2 уровень, 3 лекция
Недоступна
Типизация действий для управляющего редьюсера
Типизация действий для управляющего редьюсера
1
Задача
Модуль 3: React, 2 уровень, 3 лекция
Недоступна
Управление списком элементов с типизацией
Управление списком элементов с типизацией
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ