useReducer для сложной логики состояния

useReducer является альтернативой useState для управления состоянием компонентов с более сложной логикой. Он особенно полезен, когда состояние состоит из нескольких взаимозависимых полей или когда обновления состояния должны зависеть от предыдущего состояния.

Сигнатура хука выглядит следующим образом:

const [state, dispatch] = useReducer(reducer, initialState);
  • state — текущее состояние компонента.
  • dispatch — функция для отправки действий (actions), которая вызывает редьюсер.
  • reducer — функция, принимающая текущее состояние и действие, возвращающая новое состояние.
  • initialState — начальное значение состояния.

Функция редьюсера

Редьюсер — это чистая функция, которая описывает, как состояние изменяется в ответ на действия:

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'decrement':
      return { ...state, count: state.count - 1 };
    case 'reset':
      return { ...state, count: 0 };
    default:
      return state;
  }
}

Ключевые моменты:

  • Чистота функции: редьюсер не должен изменять состояние напрямую и не должен иметь побочных эффектов.
  • Множественные действия: через switch или объект-сопоставление можно обрабатывать разнообразные события.
  • Иммутабельность состояния: создается новый объект состояния, а не мутируется существующий.

Инициализация сложного состояния

Для сложных структур состояния удобно использовать объект или вложенные объекты:

const initialState = {
  user: {
    name: '',
    age: null
  },
  theme: 'light',
  notifications: []
};

Редьюсер для такой структуры может выглядеть так:

function reducer(state, action) {
  switch (action.type) {
    case 'updateUser':
      return { ...state, user: { ...state.user, ...action.payload } };
    case 'toggleTheme':
      return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
    case 'addNotification':
      return { ...state, notifications: [...state.notifications, action.payload] };
    default:
      return state;
  }
}

Это позволяет управлять сложным состоянием без лишней мутации и облегчает отладку.


Использование dispatch с полезными паттернами

  1. Простые действия с payload:
dispatch({ type: 'updateUser', payload: { name: 'Alice' } });
  1. Комплексные действия через функции:

Можно создать обертку для сокращения вызовов:

const updateUser = (dispatch, payload) => {
  dispatch({ type: 'updateUser', payload });
};

updateUser(dispatch, { age: 25 });
  1. Композиция действий:

Для выполнения нескольких обновлений состояния одновременно:

dispatch({ type: 'updateUser', payload: { name: 'Bob' } });
dispatch({ type: 'toggleTheme' });

Ленивая инициализация состояния

Если начальное состояние зависит от сложных вычислений, Preact поддерживает ленивую инициализацию:

const [state, dispatch] = useReducer(reducer, initialArg, initFunction);

function initFunction(initialValue) {
  return {
    ...initialValue,
    timestamp: Date.now()
  };
}

Это позволяет избежать ненужных вычислений при каждом рендере.


Практические примеры использования

Форма с несколькими полями

const initialFormState = { name: '', email: '', password: '' };

function formReducer(state, action) {
  switch (action.type) {
    case 'updateField':
      return { ...state, [action.field]: action.value };
    case 'resetForm':
      return initialFormState;
    default:
      return state;
  }
}

const [formState, dispatch] = useReducer(formReducer, initialFormState);

dispatch({ type: 'updateField', field: 'email', value: 'test@example.com' });

Список задач с динамическими действиями

const initialTodos = [];

function todosReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, action.payload];
    case 'remove':
      return state.filter(todo => todo.id !== action.payload.id);
    case 'toggle':
      return state.map(todo =>
        todo.id === action.payload.id ? { ...todo, done: !todo.done } : todo
      );
    default:
      return state;
  }
}

const [todos, dispatch] = useReducer(todosReducer, initialTodos);

dispatch({ type: 'add', payload: { id: 1, text: 'Learn Preact', done: false } });

Интеграция с эффектами

useReducer хорошо сочетается с useEffect для асинхронных действий, таких как загрузка данных:

useEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(data => dispatch({ type: 'setData', payload: data }));
}, []);

Такой подход централизует изменения состояния через dispatch, облегчая поддержку сложной логики.


Преимущества использования useReducer

  • Централизация логики состояния.
  • Улучшение читаемости и предсказуемости обновлений.
  • Легкая отладка и тестирование благодаря чистоте редьюсера.
  • Поддержка сложных структур состояния без риска непреднамеренной мутации.

useReducer становится особенно ценным инструментом в проектах с множественными взаимодействиями и динамическими обновлениями состояния, где простого useState недостаточно.