Ми вже знаємо, що редюсер — це чиста функція (state, action) => newState. Але як писати складні редюсери, коли стан — це не просто одне число, а складний об'єкт?
Перед тим як писати код, варто подумати про структуру даних.
Поганий приклад:
const initialState = {
isModalOpen: false,
userName: 'Ivan',
todos: [],
// Все на купу
filter: 'all',
loading: false
};
Хороший приклад (Групування за доменами):
const initialState = {
ui: {
isModalOpen: false,
theme: 'dark'
},
users: {
currentUser: { name: 'Ivan' },
loading: false
},
todos: {
items: [],
filter: 'all'
}
};
У цьому уроці ми зосередимося на тому, як редюсер обробляє одну частину цього стану (наприклад, todos).
Стандартний патерн — це switch statement.
const initialState = [];
function todosReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
// Логіка додавання
return [...state, action.payload];
case 'TOGGLE_TODO':
// Логіка оновлення
return state.map(todo => {
if (todo.id !== action.payload) return todo;
return { ...todo, completed: !todo.completed };
});
case 'REMOVE_TODO':
// Логіка видалення
return state.filter(todo => todo.id !== action.payload);
default:
// Завжди повертаємо поточний стан, якщо action не наш!
return state;
}
}
switch, а не if/else?Технічно різниці немає. Але switch є загальноприйнятим стандартом у спільноті Redux, оскільки він читабельніший, коли кейсів багато.
Це одна з найпотужніших можливостей Redux. Один Action може бути оброблений декількома Редюсерами одночасно!
Уявіть action LOGOUT.
usersReducer повинен очистити інформацію про користувача.todosReducer повинен очистити список завдань.uiReducer повинен скинути тему на дефолтну.Вам не потрібно діспатчити 3 різні екшени (USER_LOGOUT, TODOS_CLEAR, UI_RESET). Ви відправляєте один action LOGOUT, і кожен редюсер реагує на нього по-своєму.
// todosReducer.js
case 'LOGOUT':
return []; // Очищаємо тудушки
// usersReducer.js
case 'LOGOUT':
return null; // Видаляємо юзера
Це робить компоненти незалежними від внутрішньої структури стору. Кнопка "Вихід" просто каже "Користувач вийшов", а система сама знає, що треба почистити.
Як ми говорили раніше, оновлення глибоко вкладених даних — це пекло в класичному Redux.
case 'UPDATE_SUBTASK':
return {
...state,
items: state.items.map(item => {
if (item.id !== action.payload.itemId) return item;
return {
...item,
subtasks: item.subtasks.map(subtask => {
if (subtask.id !== action.payload.subtaskId) return subtask;
return { ...subtask, completed: true };
})
};
})
};
Це жахливо читається і легко допускаються помилки.
Порада для класичного Redux: Намагайтеся тримати стан максимально "пласким" (Flat). Уникайте глибокої вкладеності.
Якщо вам потрібно зберігати складні зв'язки (наприклад, пости і коментарі), використовуйте Нормалізацію даних (зберігати дані як у базі даних, через ID, а не вкладеними масивами).
item.subtasks[0].completed = true. Але в класичному Redux ви зобов'язані страждати (або використовувати утиліти для іммутабельності).Далі ми розглянемо, як розбивати один великий редюсер на багато маленьких.
Actions, Constants та Action Creators
У попередньому прикладі ми писали об'єкти action "на місці" і використовували рядки 'counter/increment' прямо в коді. У великому проєкті це прямий шлях до помилок.
Комбінування Reducers (Root Reducer)
У реальному додатку ви не будете тримати всю логіку в одному файлі reducer.js. Це зробило б файл безкінечним і нечитабельним.