← Назад к вопросам

Почему в Redux reducer должен быть чистой функцией?

2.0 Middle🔥 161 комментариев
#State Management

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему Redux reducer должен быть чистой функцией

Это один из ключевых принципов Redux. Чистая функция в reducerc — это залог предсказуемости, дебагирования и надёжности state management. Давайте разберёмся почему.

Что такое чистая функция

Чистая функция — это функция, которая:

  1. Для одинаковых входных данных всегда возвращает одинаковый результат
  2. Не производит побочных эффектов (side effects)
  3. Не зависит от внешнего состояния
// Чистая функция
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 должен быть чистой функцией потому что:

  1. Предсказуемость — один и тот же input = один и тот же output
  2. Time-travel debugging — можно перемотать историю
  3. Тестируемость — легко писать тесты
  4. Оптимизация — Redux может оптимизировать чистые функции

Побочные эффекты (HTTP, localStorage, console.log) — в middleware или компонентах!

Почему в Redux reducer должен быть чистой функцией? | PrepBro