Почему в Redux reducer должен быть чистой функцией?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему Redux reducer должен быть чистой функцией
Это один из ключевых принципов Redux. Чистая функция в reducerc — это залог предсказуемости, дебагирования и надёжности state management. Давайте разберёмся почему.
Что такое чистая функция
Чистая функция — это функция, которая:
- Для одинаковых входных данных всегда возвращает одинаковый результат
- Не производит побочных эффектов (side effects)
- Не зависит от внешнего состояния
// Чистая функция
function add(a, b) {
return a + b;
}
add(2, 3); // Всегда 5
add(2, 3); // Всегда 5
// НЕ чистая функция (зависит от внешней переменной)
let globalMultiplier = 2;
function multiply(a) {
return a * globalMultiplier; // зависит от globalMultiplier!
}
multiply(5); // 10
globalMultiplier = 3;
multiply(5); // 15 (другой результат при том же входе!)
Redux reducer должен быть чистой функцией
// НЕПРАВИЛЬНО (reducer с побочными эффектами)
const counterReducer = (state = 0, action) => {
if (action.type === 'INCREMENT') {
// Побочный эффект: изменяем переменную окружения
window.count++;
// Побочный эффект: запрос на сервер
fetch('/api/increment');
return state + 1;
}
return state;
};
// ПРАВИЛЬНО (чистая функция)
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
Почему это критично
1. Предсказуемость состояния
Чистая функция гарантирует, что для одинакового действия и состояния результат будет одинаковым:
const reducer = (state = { count: 0 }, action) => {
if (action.type === 'INCREMENT') {
return { count: state.count + 1 };
}
return state;
};
// Всегда можно предсказать результат
const state1 = reducer({ count: 5 }, { type: 'INCREMENT' }); // { count: 6 }
const state2 = reducer({ count: 5 }, { type: 'INCREMENT' }); // { count: 6 }
console.log(state1.count === state2.count); // true
Если reducer не чистая — результат становится непредсказуемым:
let externalCounter = 0;
const badReducer = (state = { count: 0 }, action) => {
if (action.type === 'INCREMENT') {
externalCounter++; // Побочный эффект
return { count: state.count + externalCounter };
}
return state;
};
badReducer({ count: 5 }, { type: 'INCREMENT' }); // { count: 6 }
badReducer({ count: 5 }, { type: 'INCREMENT' }); // { count: 7 } (другой результат!)
2. Time-travel debugging
Redux DevTools позволяет путешествовать по истории состояний:
// Можно "отмотать" на любой момент и посмотреть состояние
// Это работает ТОЛЬКО если reducer — чистая функция
actions: [
{ type: 'INCREMENT' },
{ type: 'INCREMENT' },
{ type: 'DECREMENT' }
]
// Благодаря чистоте можно пересчитать любую комбинацию
// и узнать состояние на любой момент времени
Если reducer имеет побочные эффекты — time-travel сломается:
const badReducer = (state = 0, action) => {
if (action.type === 'INCREMENT') {
console.log('Fetching data...'); // Побочный эффект!
return state + 1;
}
return state;
};
// При перемотке назад будут выполняться fetch-запросы снова и снова!
3. Тестируемость
Чистую функцию легко тестировать:
// Легко тестировать
const reducer = (state = { items: [] }, action) => {
if (action.type === 'ADD_ITEM') {
return {
...state,
items: [...state.items, action.payload]
};
}
return state;
};
// Тест
describe('reducer', () => {
it('добавляет элемент', () => {
const state = { items: [1, 2] };
const action = { type: 'ADD_ITEM', payload: 3 };
const result = reducer(state, action);
expect(result.items).toEqual([1, 2, 3]);
});
});
Непредсказуемый reducer невозможно протестировать:
let nextId = 1;
const badReducer = (state = { items: [] }, action) => {
if (action.type === 'ADD_ITEM') {
return {
...state,
items: [...state.items, { id: nextId++, text: action.payload }]
};
}
return state;
};
// Тест падает случайно, потому что nextId меняется при каждом вызове
4. Производительность и оптимизация
// Redux может безопасно оптимизировать чистые reducers
// Например, кэшировать результаты
const memoizedReducer = createSelector(
state => state.items,
items => items.filter(item => item.active)
);
// Это работает благодаря чистоте reducer
Где побочные эффекты ДОЛЖНЫ находиться
НЕ в reducer! Побочные эффекты должны быть в:
1. Middleware (redux-thunk, redux-saga):
// Правильно: побочный эффект в middleware
const incrementAsync = () => (dispatch) => {
fetch('/api/increment')
.then(res => res.json())
.then(data => dispatch({ type: 'INCREMENT', payload: data }));
};
dispatch(incrementAsync());
2. useEffect в React компоненте:
// Правильно: побочный эффект в useEffect
function Counter({ count, onIncrement }) {
useEffect(() => {
console.log('Count changed:', count); // Побочный эффект
}, [count]);
return (
<button onClick={onIncrement}>
Count: {count}
</button>
);
}
Классические ошибки
// НЕПРАВИЛЬНО 1: прямое мутирование state
const badReducer1 = (state = [], action) => {
if (action.type === 'ADD_ITEM') {
state.push(action.payload); // Мутируем!
return state;
}
return state;
};
// НЕПРАВИЛЬНО 2: HTTP запрос в reducer
const badReducer2 = (state = {}, action) => {
if (action.type === 'FETCH_USER') {
fetch('/api/user') // Побочный эффект!
.then(/* ... */);
return state;
}
return state;
};
// НЕПРАВИЛЬНО 3: использование текущего времени
const badReducer3 = (state = {}, action) => {
if (action.type === 'LOG') {
return { ...state, timestamp: Date.now() }; // Всегда разный результат!
}
return state;
};
// ПРАВИЛЬНО: все то же самое без побочных эффектов
const goodReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_ITEM':
return [...state, action.payload]; // Создаём новый массив
case 'FETCH_USER':
return { ...state, loading: true }; // Только обновляем состояние
case 'LOG':
return { ...state, message: action.payload }; // Используем payload
default:
return state;
}
};
Заключение
Redux reducer должен быть чистой функцией потому что:
- Предсказуемость — один и тот же input = один и тот же output
- Time-travel debugging — можно перемотать историю
- Тестируемость — легко писать тесты
- Оптимизация — Redux может оптимизировать чистые функции
Побочные эффекты (HTTP, localStorage, console.log) — в middleware или компонентах!