← React/useReducer: управление сложным состоянием#270 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

useReducer: управление сложным состоянием

Когда useState недостаточно

useState отлично справляется с простым состоянием: счётчики, флаги, строки. Но когда состояние становится сложным — много связанных полей, сложные переходы, зависимость следующего состояния от предыдущего — код начинает запутываться:

// Проблема: много useState, логика разбросана по обработчикам
function ShoppingCart() {
  const [items, setItems] = useState([])
  const [total, setTotal] = useState(0)
  const [discount, setDiscount] = useState(0)
  const [loading, setLoading] = useState(false)

  const addItem = (item) => {
    const newItems = [...items, item]
    setItems(newItems)
    // Нужно вручную синхронизировать total!
    setTotal(newItems.reduce((sum, i) => sum + i.price, 0))
  }
  // ... ещё 10 обработчиков с похожей проблемой
}

Редьюсер: паттерн управления состоянием

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

reducer(state, action) => newState
// Редьюсер описывает ВСЮ логику изменения состояния в одном месте
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      const newItems = [...state.items, action.payload]
      return {
        ...state,
        items: newItems,
        total: newItems.reduce((sum, i) => sum + i.price, 0)
      }

    case 'REMOVE_ITEM':
      const filtered = state.items.filter(i => i.id !== action.payload)
      return {
        ...state,
        items: filtered,
        total: filtered.reduce((sum, i) => sum + i.price, 0)
      }

    case 'CLEAR':
      return { items: [], total: 0, discount: 0 }

    default:
      return state  // всегда возвращаем state по умолчанию!
  }
}

useReducer в React

function ShoppingCart() {
  const initialState = { items: [], total: 0, discount: 0 }

  const [state, dispatch] = useReducer(cartReducer, initialState)

  // Диспетчеризация действий — чисто и явно
  const addItem = (item) => dispatch({ type: 'ADD_ITEM', payload: item })
  const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', payload: id })
  const clearCart = () => dispatch({ type: 'CLEAR' })

  return (
    <div>
      {state.items.map(item => (
        <CartItem key={item.id} item={item} onRemove={removeItem} />
      ))}
      <p>Итого: {state.total} ₽</p>
      <button onClick={clearCart}>Очистить</button>
    </div>
  )
}

Преимущества useReducer

1. Вся логика в одном месте — редьюсер легко читать и тестировать

2. Явные действия — по коду сразу видно что происходит

3. Чистые функции — редьюсер не зависит от внешних данных, легко тестировать

4. Предсказуемость — то же состояние + то же действие = тот же результат

useReducer + Context = мини-Redux

Комбинируя useReducer и Context, можно создать глобальное хранилище:

const StoreContext = createContext(null)

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialState)

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

// В любом компоненте
function CartBadge() {
  const { state } = useContext(StoreContext)
  return <span>{state.cart.items.length}</span>
}

function AddToCartButton({ product }) {
  const { dispatch } = useContext(StoreContext)
  return (
    <button onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}>
      В корзину
    </button>
  )
}

TypeScript: типизация reducer

// Discriminated union — TypeScript автоматически сужает тип
type Action =
  | { type: 'ADD_ITEM'; payload: CartItem }
  | { type: 'REMOVE_ITEM'; payload: number }
  | { type: 'CLEAR' }

interface CartState {
  items: CartItem[]
  total: number
}

function cartReducer(state: CartState, action: Action): CartState {
  switch (action.type) {
    case 'ADD_ITEM':
      // TypeScript знает: action.payload — это CartItem
      return { ...state, items: [...state.items, action.payload] }
    case 'REMOVE_ITEM':
      // TypeScript знает: action.payload — это number (id)
      return { ...state, items: state.items.filter(i => i.id !== action.payload) }
    case 'CLEAR':
      return { items: [], total: 0 }
  }
}

Примеры

Реализация Redux-подобного хранилища с нуля: createStore с dispatch/getState/subscribe, затем редьюсер для списка задач

// Реализуем createStore — сердце Redux — с нуля.
// useReducer в React — это упрощённая версия именно этого паттерна.

function createStore(reducer, initialState) {
  let state = initialState
  const listeners = new Set()

  return {
    // Читаем текущее состояние
    getState() {
      return state
    },

    // Отправляем действие — reducer вычисляет новое состояние
    dispatch(action) {
      const prevState = state
      state = reducer(state, action)

      // Уведомляем подписчиков только если состояние изменилось
      if (state !== prevState) {
        listeners.forEach(fn => fn(state))
      }

      console.log(`[dispatch] ${action.type}`, action.payload ?? '')
      return action
    },

    // Подписываемся на изменения (как useEffect в компонентах)
    subscribe(listener) {
      listeners.add(listener)
      return () => listeners.delete(listener)  // функция отписки
    }
  }
}

// --- Редьюсер для списка задач (ToDo) ---

const initialState = {
  todos: [],
  nextId: 1
}

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, {
          id: state.nextId,
          text: action.payload,
          completed: false
        }],
        nextId: state.nextId + 1
      }

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

    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      }

    case 'CLEAR_COMPLETED':
      return {
        ...state,
        todos: state.todos.filter(todo => !todo.completed)
      }

    default:
      return state  // важно: всегда возвращаем state по умолчанию!
  }
}

// --- Запускаем ---

const store = createStore(todoReducer, initialState)

// Подписываемся на изменения
const unsubscribe = store.subscribe(state => {
  const completed = state.todos.filter(t => t.completed).length
  console.log(`  Задач: ${state.todos.length}, выполнено: ${completed}`)
})

console.log('=== ToDo Store ===')
store.dispatch({ type: 'ADD_TODO', payload: 'Изучить useReducer' })
store.dispatch({ type: 'ADD_TODO', payload: 'Написать тесты' })
store.dispatch({ type: 'ADD_TODO', payload: 'Сделать code review' })

store.dispatch({ type: 'TOGGLE_TODO', payload: 1 })
store.dispatch({ type: 'TOGGLE_TODO', payload: 2 })

console.log('
Текущие задачи:')
store.getState().todos.forEach(t => {
  console.log(`  [${t.completed ? 'x' : ' '}] #${t.id}: ${t.text}`)
})

store.dispatch({ type: 'CLEAR_COMPLETED' })
console.log('
После очистки выполненных:')
store.getState().todos.forEach(t => {
  console.log(`  [ ] #${t.id}: ${t.text}`)
})

store.dispatch({ type: 'DELETE_TODO', payload: 3 })
console.log(
Итого задач: ${store.getState().todos.length})

unsubscribe()  // отписываемся от уведомлений

useReducer: управление сложным состоянием

Когда useState недостаточно

useState отлично справляется с простым состоянием: счётчики, флаги, строки. Но когда состояние становится сложным — много связанных полей, сложные переходы, зависимость следующего состояния от предыдущего — код начинает запутываться:

// Проблема: много useState, логика разбросана по обработчикам
function ShoppingCart() {
  const [items, setItems] = useState([])
  const [total, setTotal] = useState(0)
  const [discount, setDiscount] = useState(0)
  const [loading, setLoading] = useState(false)

  const addItem = (item) => {
    const newItems = [...items, item]
    setItems(newItems)
    // Нужно вручную синхронизировать total!
    setTotal(newItems.reduce((sum, i) => sum + i.price, 0))
  }
  // ... ещё 10 обработчиков с похожей проблемой
}

Редьюсер: паттерн управления состоянием

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

reducer(state, action) => newState
// Редьюсер описывает ВСЮ логику изменения состояния в одном месте
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      const newItems = [...state.items, action.payload]
      return {
        ...state,
        items: newItems,
        total: newItems.reduce((sum, i) => sum + i.price, 0)
      }

    case 'REMOVE_ITEM':
      const filtered = state.items.filter(i => i.id !== action.payload)
      return {
        ...state,
        items: filtered,
        total: filtered.reduce((sum, i) => sum + i.price, 0)
      }

    case 'CLEAR':
      return { items: [], total: 0, discount: 0 }

    default:
      return state  // всегда возвращаем state по умолчанию!
  }
}

useReducer в React

function ShoppingCart() {
  const initialState = { items: [], total: 0, discount: 0 }

  const [state, dispatch] = useReducer(cartReducer, initialState)

  // Диспетчеризация действий — чисто и явно
  const addItem = (item) => dispatch({ type: 'ADD_ITEM', payload: item })
  const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', payload: id })
  const clearCart = () => dispatch({ type: 'CLEAR' })

  return (
    <div>
      {state.items.map(item => (
        <CartItem key={item.id} item={item} onRemove={removeItem} />
      ))}
      <p>Итого: {state.total} ₽</p>
      <button onClick={clearCart}>Очистить</button>
    </div>
  )
}

Преимущества useReducer

1. Вся логика в одном месте — редьюсер легко читать и тестировать

2. Явные действия — по коду сразу видно что происходит

3. Чистые функции — редьюсер не зависит от внешних данных, легко тестировать

4. Предсказуемость — то же состояние + то же действие = тот же результат

useReducer + Context = мини-Redux

Комбинируя useReducer и Context, можно создать глобальное хранилище:

const StoreContext = createContext(null)

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialState)

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

// В любом компоненте
function CartBadge() {
  const { state } = useContext(StoreContext)
  return <span>{state.cart.items.length}</span>
}

function AddToCartButton({ product }) {
  const { dispatch } = useContext(StoreContext)
  return (
    <button onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}>
      В корзину
    </button>
  )
}

TypeScript: типизация reducer

// Discriminated union — TypeScript автоматически сужает тип
type Action =
  | { type: 'ADD_ITEM'; payload: CartItem }
  | { type: 'REMOVE_ITEM'; payload: number }
  | { type: 'CLEAR' }

interface CartState {
  items: CartItem[]
  total: number
}

function cartReducer(state: CartState, action: Action): CartState {
  switch (action.type) {
    case 'ADD_ITEM':
      // TypeScript знает: action.payload — это CartItem
      return { ...state, items: [...state.items, action.payload] }
    case 'REMOVE_ITEM':
      // TypeScript знает: action.payload — это number (id)
      return { ...state, items: state.items.filter(i => i.id !== action.payload) }
    case 'CLEAR':
      return { items: [], total: 0 }
  }
}

Примеры

Реализация Redux-подобного хранилища с нуля: createStore с dispatch/getState/subscribe, затем редьюсер для списка задач

// Реализуем createStore — сердце Redux — с нуля.
// useReducer в React — это упрощённая версия именно этого паттерна.

function createStore(reducer, initialState) {
  let state = initialState
  const listeners = new Set()

  return {
    // Читаем текущее состояние
    getState() {
      return state
    },

    // Отправляем действие — reducer вычисляет новое состояние
    dispatch(action) {
      const prevState = state
      state = reducer(state, action)

      // Уведомляем подписчиков только если состояние изменилось
      if (state !== prevState) {
        listeners.forEach(fn => fn(state))
      }

      console.log(`[dispatch] ${action.type}`, action.payload ?? '')
      return action
    },

    // Подписываемся на изменения (как useEffect в компонентах)
    subscribe(listener) {
      listeners.add(listener)
      return () => listeners.delete(listener)  // функция отписки
    }
  }
}

// --- Редьюсер для списка задач (ToDo) ---

const initialState = {
  todos: [],
  nextId: 1
}

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, {
          id: state.nextId,
          text: action.payload,
          completed: false
        }],
        nextId: state.nextId + 1
      }

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

    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      }

    case 'CLEAR_COMPLETED':
      return {
        ...state,
        todos: state.todos.filter(todo => !todo.completed)
      }

    default:
      return state  // важно: всегда возвращаем state по умолчанию!
  }
}

// --- Запускаем ---

const store = createStore(todoReducer, initialState)

// Подписываемся на изменения
const unsubscribe = store.subscribe(state => {
  const completed = state.todos.filter(t => t.completed).length
  console.log(`  Задач: ${state.todos.length}, выполнено: ${completed}`)
})

console.log('=== ToDo Store ===')
store.dispatch({ type: 'ADD_TODO', payload: 'Изучить useReducer' })
store.dispatch({ type: 'ADD_TODO', payload: 'Написать тесты' })
store.dispatch({ type: 'ADD_TODO', payload: 'Сделать code review' })

store.dispatch({ type: 'TOGGLE_TODO', payload: 1 })
store.dispatch({ type: 'TOGGLE_TODO', payload: 2 })

console.log('
Текущие задачи:')
store.getState().todos.forEach(t => {
  console.log(`  [${t.completed ? 'x' : ' '}] #${t.id}: ${t.text}`)
})

store.dispatch({ type: 'CLEAR_COMPLETED' })
console.log('
После очистки выполненных:')
store.getState().todos.forEach(t => {
  console.log(`  [ ] #${t.id}: ${t.text}`)
})

store.dispatch({ type: 'DELETE_TODO', payload: 3 })
console.log(
Итого задач: ${store.getState().todos.length})

unsubscribe()  // отписываемся от уведомлений

Задание

Создай компонент App со списком задач на useReducer. Редьюсер должен обрабатывать три действия: ADD (добавить задачу по тексту из поля ввода), TOGGLE (отметить выполненной по id), DELETE (удалить по id). Отображай список задач с чекбоксами и кнопками удаления.

Подсказка

useReducer(todoReducer, initialState) возвращает [state, dispatch]. dispatch({ type: "ADD", payload: text }) добавляет задачу. В TOGGLE используй !t.completed. В DELETE используй .filter(). nextId увеличивай на 1 в каждом ADD.

Загружаем среду выполнения...
Загружаем AI-помощника...