JavaRush /Курсы /Модуль 3: React /Создание редьюсера с useReducer для сложного состояния — ...

Создание редьюсера с useReducer для сложного состояния — шаги и структура

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

Создания редьюсера

В предыдущих лекциях мы изучили основы хуков useState и useReducer, обсудили их различия и поняли, в каких случаях каждый из них лучше использовать. Мы также успели разобраться, как эффективно типизировать состояния и действия редьюсера с помощью TypeScript. Теперь, с этими знаниями, мы готовы создать полноценный редьюсер и организовать логику работы с более сложным состоянием.

Редьюсер можно представить как дирижёра оркестра: он принимает входящие действия actions и решает, как изменить состояние state. Давайте разберёмся, как создать редьюсер для управления сложным состоянием.

1. Определяем начальное состояние

Первый шаг в создании редьюсера — это определение начального состояния, которое будет представлять собой "нулевую точку отсчёта". В нашем случае это объект, описывающий текущее состояние нашего приложения.

Пример начального состояния:

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface State {
  todos: Todo[];
  filter: 'all' | 'completed' | 'active';
}

const initialState: State = {
  todos: [],
  filter: 'all',
};

 

Здесь мы описываем список задач (todos) и выбранный фильтр для отображения задач.

2. Определяем действия (actions)

Действия — это то, что передаёт информацию о том, какие изменения должны произойти со состоянием. В согласии со строгой типизацией, каждое действие описывается интерфейсом.

type Action =
  | { type: 'ADD_TODO'; payload: string } // Добавить задачу
  | { type: 'TOGGLE_TODO'; payload: number } // Переключить статус задачи
  | { type: 'SET_FILTER'; payload: 'all' | 'completed' | 'active' }; // Установить фильтр

Каждое действие здесь строго типизировано, что предотвращает ошибки, как если бы вы передали неправильные данные в dispatch.

3. Создание функции-редьюсера

Редьюсер — это чистая функция, которая принимает текущее состояние и действие, а затем возвращает новое состояние. Она должна быть готова к обработке любых определённых действий.

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.payload, completed: false },
        ],
      };

    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
        ),
      };

    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload,
      };

    default:
      return state; // На всякий случай, если действие не распознано
  }
};

Каждый case описывает, как изменится состояние в зависимости от полученного действия.

Структура сложного редьюсера

Организация логики редьюсера

Когда список действий становится длинным, редьюсер может быстро разрастись, превратившись в адское месиво из switch-case. Чтобы этого избежать, можно использовать вспомогательные функции.

Пример выделения логики в функции:

const addTodo = (state: State, text: string): State =v ({
  ...state,
  todos: [...state.todos, { id: Date.now(), text, completed: false }],
});

const toggleTodo = (state: State, id: number): State => ({
  ...state,
  todos: state.todos.map((todo) =>
    todo.id === id ? { ...todo, completed: !todo.completed } : todo
  ),
});

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'ADD_TODO':
      return addTodo(state, action.payload);
    case 'TOGGLE_TODO':
      return toggleTodo(state, action.payload);
    case 'SET_FILTER':
      return { ...state, filter: action.payload };
    default:
      return state;
  }
};

Это делает код более читаемым и поддерживаемым.

Работа с вложенными объектами

Иногда состояние приложения является вложенным, например, если у задачи есть отдельные атрибуты, которые нужно обновлять. Отправляться в кодовый ад, перезаписывая вручную каждый уровень вложенности, не стоит.

Пример работы с вложенной структурой:

const updateTodoText = (state: State, id: number, newText: string): State => ({
  ...state,
  todos: state.todos.map((todo) =>
    todo.id === id ? { ...todo, text: newText } : todo
  ),
});

Фокусировка на неизменяемости

Важное правило Redux и useReducer — не изменяйте состояние напрямую. Например, нельзя делать что-то вроде:

state.todos[0].text = 'New Text'; // ❌ Ошибка!

Вместо этого используем методы, вроде map или spread-оператора, чтобы создавать новое состояние без модификации оригинального.

Практическая реализация: чек-лист приложения

Давайте соберём всё вместе! Создадим простое приложение "Чек-лист задач" с использованием useReducer.

Шаг 1. Создаём редьюсер

Редьюсер уже готов, он определён выше. Теперь переходим к его интеграции.

Шаг 2. Используем useReducer в компоненте

В React-компонентах useReducer используется для управления состоянием.

import React, { useReducer } from 'react';

const TodoApp: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const addTodo = () => {
    const text = prompt('Введите задачу:');
    if (text) {
      dispatch({ type: 'ADD_TODO', payload: text });
    }
  };

  return (
    <div>
      <h1>Чек-лист задач</h1>
      <button onClick={addTodo}>Добавить задачу</button>
      <ul>
        {state.todos.map((todo) => (
          <li
                  key={todo.id}
                  style={{
                  textDecoration: todo.completed ? 'line-through' : 'none',
            }}
            onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
          >
            {todo.text}
        </li>
        ))}
      </ul>
      <div>
        <button onClick={() => dispatch({ type: 'SET_FILTER', payload: 'all' })}>
          Все
        </button>
        <button
                onClick={() => dispatch({ type: 'SET_FILTER', payload: 'completed' })}
        >
          Завершённые
        </button>
        <button onClick={() => dispatch({ type: 'SET_FILTER', payload: 'active' })}>
          Активные
        </button>
      </div>
    </div>
  );
};

export default TodoApp;

Шаг 3. Добавляем условие для фильтрации задач

В этом компоненте мы можем дополнительно отфильтровывать задачи перед их отображением.

const filteredTodos = state.todos.filter((todo) => {
  if (state.filter === 'all') return true;
  if (state.filter === 'completed') return todo.completed;
  if (state.filter === 'active') return !todo.completed;
});

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