Создания редьюсера
В предыдущих лекциях мы изучили основы хуков 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;
});
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ