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

Как работает UseReducer?

2.2 Middle🔥 241 комментариев
#React

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

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

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

Как работает useReducer в React

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

1. Базовая концепция

import { useReducer } from 'react';

// Reducer - чистая функция, которая определяет как меняется state
const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    default:
      return state;
  }
};

// В компоненте
const Counter = () => {
  const [state, dispatch] = useReducer(
    reducer,
    { count: 0 } // initial state
  );

  return (
    <div>
      <p>Счёт: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>
        +
      </button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>
        -
      </button>
      <button onClick={() => dispatch({ type: 'RESET' })}>
        Сброс
      </button>
    </div>
  );
};

2. Структура и синтаксис

// useReducer(reducer, initialState, init?)
const [state, dispatch] = useReducer(reducer, initialValue, init);

// reducer - функция (state, action) => newState
// initialState - начальное значение
// init - опциональная функция инициализации

// action обычно имеет вид:
const action = {
  type: 'ACTION_NAME',    // обязательно
  payload: { data }        // опциональные данные
};

3. Пример с payload

const initialState = {
  todos: [],
  filter: 'all',
  loading: false
};

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.payload, done: false }
        ]
      };

    case 'REMOVE_TODO':
      return {
        ...state,
        todos: state.todos.filter(t => t.id !== action.payload)
      };

    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(t =>
          t.id === action.payload ? { ...t, done: !t.done } : t
        )
      };

    case 'SET_FILTER':
      return { ...state, filter: action.payload };

    case 'SET_LOADING':
      return { ...state, loading: action.payload };

    default:
      return state;
  }
};

const TodoApp = () => {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  const handleAddTodo = (text) => {
    dispatch({ type: 'ADD_TODO', payload: text });
  };

  const handleRemove = (id) => {
    dispatch({ type: 'REMOVE_TODO', payload: id });
  };

  const handleToggle = (id) => {
    dispatch({ type: 'TOGGLE_TODO', payload: id });
  };

  return (
    <div>
      <input
        onKeyPress={(e) => {
          if (e.key === 'Enter') {
            handleAddTodo(e.target.value);
            e.target.value = '';
          }
        }}
        placeholder='Новая задача'
      />
      {state.todos.map(todo => (
        <div key={todo.id}>
          <input
            type='checkbox'
            checked={todo.done}
            onChange={() => handleToggle(todo.id)}
          />
          <span>{todo.text}</span>
          <button onClick={() => handleRemove(todo.id)}>Удалить</button>
        </div>
      ))}
    </div>
  );
};

4. Функция инициализации (init)

Опциональный третий параметр для ленивой инициализации:

const init = (initialCount) => {
  // Сложная логика инициализации
  return {
    count: initialCount,
    timestamp: Date.now()
  };
};

const [state, dispatch] = useReducer(
  reducer,
  0,        // initialArg
  init      // функция инициализации
);

// init вызовется только один раз при монтировании

5. Константы для типов действий

Хороший стиль - использовать константы:

// actions.ts
export const TODO_ACTIONS = {
  ADD: 'TODO_ADD',
  REMOVE: 'TODO_REMOVE',
  TOGGLE: 'TODO_TOGGLE',
  SET_FILTER: 'TODO_SET_FILTER'
};

// reducer.ts
const todoReducer = (state, action) => {
  switch (action.type) {
    case TODO_ACTIONS.ADD:
      // ...
    case TODO_ACTIONS.REMOVE:
      // ...
    default:
      return state;
  }
};

// component.ts
dispatch({ type: TODO_ACTIONS.ADD, payload: text });

6. Асинхронные операции с useReducer

const initialState = {
  data: null,
  loading: false,
  error: null
};

const dataReducer = (state, action) => {
  switch (action.type) {
    case 'LOADING':
      return { ...state, loading: true, error: null };
    case 'SUCCESS':
      return { data: action.payload, loading: false, error: null };
    case 'ERROR':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
};

const DataFetcher = () => {
  const [state, dispatch] = useReducer(dataReducer, initialState);

  const fetchData = async (url) => {
    dispatch({ type: 'LOADING' });
    try {
      const response = await fetch(url);
      const data = await response.json();
      dispatch({ type: 'SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'ERROR', payload: error.message });
    }
  };

  if (state.loading) return <div>Загрузка...</div>;
  if (state.error) return <div>Ошибка: {state.error}</div>;
  return <div>{JSON.stringify(state.data)}</div>;
};

7. useReducer vs useState

// ❌ useState для сложной логики - становится запутанным
const Complex = () => {
  const [count, setCount] = useState(0);
  const [history, setHistory] = useState([]);
  const [canUndo, setCanUndo] = useState(false);

  const increment = () => {
    setHistory([...history, count]);
    setCount(count + 1);
    setCanUndo(history.length > 0);
  };
  // Логика разбросана, сложно отследить
};

// ✅ useReducer для сложной логики - ясно и организовано
const Complex = () => {
  const [state, dispatch] = useReducer(complexReducer, initialState);

  const increment = () => {
    dispatch({ type: 'INCREMENT' });
  };
  // Вся логика в одном месте (reducer)
};

8. Когда использовать useReducer

// Используй useReducer если:

// 1. Множество связанных state переменных
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [address, setAddress] = useState('');
// -> Лучше одно состояние в reducer

// 2. Сложная логика обновления
const handleSubmit = () => {
  validate();
  transform();
  submit();
  reset();
};
// -> Каждый шаг - отдельный action в reducer

// 3. Нужна история состояний
const [history, setHistory] = useState([]);
const undo = () => {
  setHistory(history.slice(0, -1));
  setState(history[history.length - 2]);
};
// -> Reducer может хранить историю

// 4. Передача dispatch в контекст
const [state, dispatch] = useReducer(reducer, initial);
return (
  <AppContext.Provider value={{ state, dispatch }}>
    {children}
  </AppContext.Provider>
);
// Легко передавать одну функцию вместо множества setState

9. useReducer с Context для глобального состояния

const AppContext = createContext();

const AppProvider = ({ children }) => {
  const [state, dispatch] = useReducer(appReducer, initialState);

  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};

const useAppContext = () => {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useAppContext должен быть внутри AppProvider');
  }
  return context;
};

// В компоненте
const ChildComponent = () => {
  const { state, dispatch } = useAppContext();
  return (
    <button onClick={() => dispatch({ type: 'INCREMENT' })}>
      {state.count}
    </button>
  );
};

10. Тестирование reducer

import { describe, it, expect } from 'vitest';
import { todoReducer } from './reducer';

describe('todoReducer', () => {
  it('добавляет новую задачу', () => {
    const state = { todos: [] };
    const action = { type: 'ADD_TODO', payload: 'Купить молоко' };
    const newState = todoReducer(state, action);

    expect(newState.todos).toHaveLength(1);
    expect(newState.todos[0].text).toBe('Купить молоко');
  });

  it('удаляет задачу по id', () => {
    const state = {
      todos: [{ id: 1, text: 'Task' }]
    };
    const action = { type: 'REMOVE_TODO', payload: 1 };
    const newState = todoReducer(state, action);

    expect(newState.todos).toHaveLength(0);
  });
});

Ключевые преимущества useReducer

  1. Предсказуемость - все изменения state проходят через reducer
  2. Тестируемость - reducer это чистая функция, легко тестировать
  3. Масштабируемость - удобно когда состояние усложняется
  4. Отладка - видна история всех actions
  5. Переиспользование - одного reducer для нескольких компонентов

Вывод: useReducer это мощный инструмент для управления сложным состоянием, вторая опция после useState.