useReducer 你可以把它当成 “更规范、更可控的 useState”

当你的状态变复杂、更新方式很多、或者你想把“怎么改状态”集中管理时,用它会非常爽。


1)useReducer 是什么(核心概念)

useReducer 的核心思想来源于 Flux/Redux 架构,它遵循 “action -> reducer -> new state” 的数据流。其基本形式由三部分组成:

  1. state:当前状态

  2. dispatch(action):你发出一个“指令”(action)

  3. reducer(state, action):一个纯函数,根据指令,接收当前状态和动作,并返回新的状态。(state, action) => newState

  4. initialState:状态的初始值。

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

记住一句话:dispatch 发动作,reducer 负责算新状态。


2)为什么要用 useReducer,而不是 useState?

useState 适合:

  • 状态简单(1~2 个字段)

  • 更新方式少(就 setX)

useReducer 适合:

  • 状态是对象/数组,字段多

  • 更新逻辑分散在很多地方,容易乱

  • “更新方式”很多(新增/删除/切换/编辑/批量更新/回滚)

  • 想要:所有状态变化都走同一套“动作(Action) → 规则(Reducer)”

一个经典场景:

add / toggle / delete 三种操作,如果继续扩展成 edit / clearCompleted / reorder,用 useState 会越来越乱。


3)最小例子:计数器(理解 dispatch + reducer)

const ACTIONS = {
  INC: "inc",
  DEC: "dec",
}

function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.INC:
      return { count: state.count + 1 }
    case ACTIONS.DEC:
      return { count: state.count - 1 }
    default:
      return state
  }
}

export default function Counter() {
  const [state, dispatch] = React.useReducer(reducer, { count: 0 })

  return (
    <>
      <button onClick={() => dispatch({ type: ACTIONS.DEC })}>-</button>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: ACTIONS.INC })}>+</button>
    </>
  )
}

这里的重点不是“写起来更长”,而是:

你把“如何更新”从组件里抽走了,组件只负责“发动作”。


4)带 payload:把参数一起发给 reducer

比如 Todo:新增时需要 name,toggle/delete 需要 id。

dispatch({ type: "add", payload: { name } })
dispatch({ type: "toggle", payload: { id } })
dispatch({ type: "delete", payload: { id } })

reducer 里拿到 action.payload 来做事。


5)你真正会遇到的实战例子

例 1:表单很多字段 + 校验 + 重置

场景:登录/注册/创建项目表单

动作:setField、setError、reset、submitStart/Success/Fail

function reducer(state, action) {
  switch (action.type) {
    case "setField":
      return { ...state, values: { ...state.values, [action.field]: action.value } }
    case "setError":
      return { ...state, errors: { ...state.errors, [action.field]: action.error } }
    case "reset":
      return action.initial
    default:
      return state
  }
}

好处:表单逻辑集中,不会散落在 10 个 setState 里。


例 2:请求状态机(loading/error/data + 取消/重试)

场景:拉取列表、分页、刷新

动作:fetchStart / fetchSuccess / fetchError / reset

function reducer(state, action) {
  switch (action.type) {
    case "fetchStart":
      return { ...state, loading: true, error: null }
    case "fetchSuccess":
      return { loading: false, error: null, data: action.data }
    case "fetchError":
      return { ...state, loading: false, error: action.error }
    default:
      return state
  }
}

你会发现:useReducer 非常适合“状态机”这种结构。


例 3:Web3 钱包连接状态(连接/断开/切链/更新余额)

state:address, chainId, status, balance, error
actions:connectStart/connectOk/connectFail/disconnect/chainChanged/balanceUpdated

WalletProvider.tsx

import React, { createContext, useContext, useEffect, useReducer } from "react"

// 1) 类型
type WalletStatus = "idle" | "connecting" | "connected" | "error"

type WalletState = {
  address: string | null
  chainId: number | null
  status: WalletStatus
  balance: string | null // 用 string 存,避免浮点问题(ETH/Wei)
  error: string | null
}

type Action =
  | { type: "connectStart" }
  | { type: "connectOk"; payload: { address: string; chainId: number } }
  | { type: "connectFail"; payload: { error: string } }
  | { type: "disconnect" }
  | { type: "chainChanged"; payload: { chainId: number } }
  | { type: "balanceUpdated"; payload: { balance: string } }

const initialState: WalletState = {
  address: null,
  chainId: null,
  status: "idle",
  balance: null,
  error: null,
}

// 2) reducer:只负责“怎么算新 state”
function walletReducer(state: WalletState, action: Action): WalletState {
  switch (action.type) {
    case "connectStart":
      return { ...state, status: "connecting", error: null }

    case "connectOk":
      return {
        ...state,
        status: "connected",
        error: null,
        address: action.payload.address,
        chainId: action.payload.chainId,
      }

    case "connectFail":
      return { ...state, status: "error", error: action.payload.error }

    case "disconnect":
      return { ...initialState }

    case "chainChanged":
      // 切链后一般余额要重新拉
      return { ...state, chainId: action.payload.chainId, balance: null }

    case "balanceUpdated":
      return { ...state, balance: action.payload.balance }

    default:
      return state
  }
}

// 3) Context
type WalletContextValue = {
  state: WalletState
  dispatch: React.Dispatch<Action>
  connect: () => Promise<void>
  disconnect: () => void
  refreshBalance: () => Promise<void>
}

const WalletContext = createContext<WalletContextValue | null>(null)

export function useWallet() {
  const ctx = useContext(WalletContext)
  if (!ctx) throw new Error("useWallet must be used within WalletProvider")
  return ctx
}

// 4) Provider:副作用(连接钱包/监听事件/请求余额)放这里或单独 hooks
export function WalletProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(walletReducer, initialState)

  // --- 你项目里可以换成 ethers / wagmi / viem ---
  const getProvider = () => (window as any).ethereum as any | undefined

  const connect = async () => {
    const eth = getProvider()
    if (!eth) {
      dispatch({ type: "connectFail", payload: { error: "No wallet found (window.ethereum missing)" } })
      return
    }

    dispatch({ type: "connectStart" })

    try {
      // 请求账号
      const accounts: string[] = await eth.request({ method: "eth_requestAccounts" })
      const address = accounts?.[0]
      if (!address) throw new Error("No account returned")

      // 获取 chainId(hex -> number)
      const chainIdHex: string = await eth.request({ method: "eth_chainId" })
      const chainId = parseInt(chainIdHex, 16)

      dispatch({ type: "connectOk", payload: { address, chainId } })
    } catch (e: any) {
      dispatch({ type: "connectFail", payload: { error: e?.message ?? "Connect failed" } })
    }
  }

  const disconnect = () => {
    // 注意:EIP-1193 没有统一的“主动断开”标准,通常是本地清状态
    dispatch({ type: "disconnect" })
  }

  const refreshBalance = async () => {
    const eth = getProvider()
    if (!eth || !state.address) return

    try {
      // 这里只演示用 RPC 方法拿余额(wei 的 hex)
      const balanceHex: string = await eth.request({
        method: "eth_getBalance",
        params: [state.address, "latest"],
      })
      // 这里简单转成 BigInt string(你可以用 ethers.formatEther)
      const wei = BigInt(balanceHex)
      dispatch({ type: "balanceUpdated", payload: { balance: wei.toString() } })
    } catch (e: any) {
      // 拉余额失败不一定要把 status 变 error,看你需求
      dispatch({ type: "connectFail", payload: { error: e?.message ?? "Balance fetch failed" } })
    }
  }

  // 监听钱包事件:accountsChanged / chainChanged
  useEffect(() => {
    const eth = getProvider()
    if (!eth) return

    const onAccountsChanged = (accounts: string[]) => {
      const next = accounts?.[0] ?? null
      if (!next) {
        dispatch({ type: "disconnect" })
        return
      }
      // accountsChanged 通常不会给 chainId,这里读一下
      eth.request({ method: "eth_chainId" }).then((chainIdHex: string) => {
        dispatch({
          type: "connectOk",
          payload: { address: next, chainId: parseInt(chainIdHex, 16) },
        })
      })
    }

    const onChainChanged = (chainIdHex: string) => {
      dispatch({ type: "chainChanged", payload: { chainId: parseInt(chainIdHex, 16) } })
    }

    eth.on?.("accountsChanged", onAccountsChanged)
    eth.on?.("chainChanged", onChainChanged)

    return () => {
      eth.removeListener?.("accountsChanged", onAccountsChanged)
      eth.removeListener?.("chainChanged", onChainChanged)
    }
  }, [])

  // 连上后自动拉余额
  useEffect(() => {
    if (state.status === "connected" && state.address) {
      refreshBalance()
    }
  }, [state.status, state.address, state.chainId])

  const value: WalletContextValue = { state, dispatch, connect, disconnect, refreshBalance }

  return <WalletContext.Provider value={value}>{children}</WalletContext.Provider>
}

WalletPanel.tsx(使用)

import React from "react"
import { useWallet } from "./WalletProvider"

export function WalletPanel() {
  const { state, connect, disconnect, refreshBalance } = useWallet()

  return (
    <div style={{ padding: 12, border: "1px solid #ddd" }}>
      <div>Status: {state.status}</div>
      <div>Address: {state.address ?? "-"}</div>
      <div>ChainId: {state.chainId ?? "-"}</div>
      <div>Balance(wei): {state.balance ?? "-"}</div>
      {state.error && <div style={{ color: "red" }}>Error: {state.error}</div>}

      <div style={{ marginTop: 8, display: "flex", gap: 8 }}>
        {state.status !== "connected" ? (
          <button onClick={connect}>Connect</button>
        ) : (
          <>
            <button onClick={refreshBalance}>Refresh Balance</button>
            <button onClick={disconnect}>Disconnect</button>
          </>
        )}
      </div>
    </div>
  )
}

这个比 useState 更清晰,因为“允许发生的变化”是你定义的动作集合。


例 4: 复杂列表(批量/全选/排序/移动分组/撤销重做)useReducer

我们做一个“任务列表 + 分组”的 reducer,支持:

  • CRUD:add/update/delete

  • 批量:select、selectAll、clearSelection、bulkDelete、bulkMove

  • 排序:按 name/createdAt

  • 撤销/重做:undo/redo(用 history 包一层)

listReducer.ts

// 1) 数据结构
export type Item = {
  id: string
  name: string
  groupId: string
  createdAt: number
  done: boolean
}

type SortKey = "createdAt" | "name"
type SortOrder = "asc" | "desc"

export type ListState = {
  items: Item[]
  selectedIds: Record<string, true> // 用 map 更快
  sort: { key: SortKey; order: SortOrder }
}

const initialPresent: ListState = {
  items: [],
  selectedIds: {},
  sort: { key: "createdAt", order: "desc" },
}

// 2) action
export type ListAction =
  | { type: "add"; payload: { name: string; groupId: string } }
  | { type: "update"; payload: { id: string; patch: Partial<Omit<Item, "id" | "createdAt">> } }
  | { type: "delete"; payload: { id: string } }

  | { type: "toggleSelect"; payload: { id: string } }
  | { type: "selectAll" }
  | { type: "clearSelection" }

  | { type: "bulkDeleteSelected" }
  | { type: "bulkMoveSelected"; payload: { groupId: string } }

  | { type: "setSort"; payload: { key: SortKey; order: SortOrder } }

  | { type: "undo" }
  | { type: "redo" }

// 3) 工具
const uid = () => Math.random().toString(36).slice(2) + Date.now().toString(36)

function sortItems(items: Item[], sort: ListState["sort"]) {
  const { key, order } = sort
  const dir = order === "asc" ? 1 : -1
  return [...items].sort((a, b) => {
    if (key === "createdAt") return (a.createdAt - b.createdAt) * dir
    // name
    return a.name.localeCompare(b.name) * dir
  })
}

// 4) 纯 reducer:只处理 present
function presentReducer(state: ListState, action: ListAction): ListState {
  switch (action.type) {
    case "add": {
      const item: Item = {
        id: uid(),
        name: action.payload.name,
        groupId: action.payload.groupId,
        createdAt: Date.now(),
        done: false,
      }
      const items = sortItems([...state.items, item], state.sort)
      return { ...state, items }
    }

    case "update": {
      const { id, patch } = action.payload
      const items = state.items.map((it) => (it.id === id ? { ...it, ...patch } : it))
      return { ...state, items: sortItems(items, state.sort) }
    }

    case "delete": {
      const { id } = action.payload
      const items = state.items.filter((it) => it.id !== id)
      const nextSelected = { ...state.selectedIds }
      delete nextSelected[id]
      return { ...state, items, selectedIds: nextSelected }
    }

    case "toggleSelect": {
      const { id } = action.payload
      const selectedIds = { ...state.selectedIds }
      if (selectedIds[id]) delete selectedIds[id]
      else selectedIds[id] = true
      return { ...state, selectedIds }
    }

    case "selectAll": {
      const selectedIds: Record<string, true> = {}
      for (const it of state.items) selectedIds[it.id] = true
      return { ...state, selectedIds }
    }

    case "clearSelection":
      return { ...state, selectedIds: {} }

    case "bulkDeleteSelected": {
      const ids = state.selectedIds
      const items = state.items.filter((it) => !ids[it.id])
      return { ...state, items, selectedIds: {} }
    }

    case "bulkMoveSelected": {
      const targetGroupId = action.payload.groupId
      const ids = state.selectedIds
      const items = state.items.map((it) => (ids[it.id] ? { ...it, groupId: targetGroupId } : it))
      return { ...state, items: sortItems(items, state.sort), selectedIds: {} }
    }

    case "setSort": {
      const sort = action.payload
      return { ...state, sort, items: sortItems(state.items, sort) }
    }

    default:
      return state
  }
}

// 5) history 包装:支持 undo/redo
type HistoryState = {
  past: ListState[]
  present: ListState
  future: ListState[]
}

export const initialHistoryState: HistoryState = {
  past: [],
  present: initialPresent,
  future: [],
}

function isHistoryFreeAction(action: ListAction) {
  // 选择/清选择这种“UI态”你也可以选择不进历史,看你需求
  return action.type === "toggleSelect" || action.type === "selectAll" || action.type === "clearSelection"
}

export function listReducerWithHistory(state: HistoryState, action: ListAction): HistoryState {
  switch (action.type) {
    case "undo": {
      if (state.past.length === 0) return state
      const previous = state.past[state.past.length - 1]
      const past = state.past.slice(0, -1)
      return { past, present: previous, future: [state.present, ...state.future] }
    }

    case "redo": {
      if (state.future.length === 0) return state
      const next = state.future[0]
      const future = state.future.slice(1)
      return { past: [...state.past, state.present], present: next, future }
    }

    default: {
      const newPresent = presentReducer(state.present, action)

      // 没变化:不推进历史
      if (newPresent === state.present) return state

      // 有些 action 你可能不想进历史(比如选择)
      if (isHistoryFreeAction(action)) {
        return { ...state, present: newPresent }
      }

      // 正常推进历史:past +1,future 清空
      return {
        past: [...state.past, state.present],
        present: newPresent,
        future: [],
      }
    }
  }
}

ListDemo.tsx(使用示例)

import React, { useReducer, useState } from "react"
import { initialHistoryState, listReducerWithHistory } from "./listReducer"

export function ListDemo() {
  const [state, dispatch] = useReducer(listReducerWithHistory, initialHistoryState)
  const present = state.present

  const [name, setName] = useState("")
  const [groupId, setGroupId] = useState("group-a")

  const selectedCount = Object.keys(present.selectedIds).length
  const canUndo = state.past.length > 0
  const canRedo = state.future.length > 0

  return (
    <div style={{ padding: 12 }}>
      <h3>Complex List Demo</h3>

      <div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
        <input value={name} onChange={(e) => setName(e.target.value)} placeholder="item name" />
        <select value={groupId} onChange={(e) => setGroupId(e.target.value)}>
          <option value="group-a">group-a</option>
          <option value="group-b">group-b</option>
          <option value="group-c">group-c</option>
        </select>
        <button
          onClick={() => {
            if (!name.trim()) return
            dispatch({ type: "add", payload: { name: name.trim(), groupId } })
            setName("")
          }}
        >
          Add
        </button>
      </div>

      <div style={{ display: "flex", gap: 8, marginBottom: 8, flexWrap: "wrap" }}>
        <button onClick={() => dispatch({ type: "selectAll" })}>Select All</button>
        <button onClick={() => dispatch({ type: "clearSelection" })}>Clear Selection</button>
        <button disabled={selectedCount === 0} onClick={() => dispatch({ type: "bulkDeleteSelected" })}>
          Bulk Delete ({selectedCount})
        </button>
        <button
          disabled={selectedCount === 0}
          onClick={() => dispatch({ type: "bulkMoveSelected", payload: { groupId: "group-b" } })}
        >
          Move Selected → group-b
        </button>

        <button disabled={!canUndo} onClick={() => dispatch({ type: "undo" })}>Undo</button>
        <button disabled={!canRedo} onClick={() => dispatch({ type: "redo" })}>Redo</button>

        <button onClick={() => dispatch({ type: "setSort", payload: { key: "createdAt", order: "desc" } })}>
          Sort: createdAt desc
        </button>
        <button onClick={() => dispatch({ type: "setSort", payload: { key: "name", order: "asc" } })}>
          Sort: name asc
        </button>
      </div>

      <ul>
        {present.items.map((it) => (
          <li key={it.id} style={{ display: "flex", gap: 8, alignItems: "center" }}>
            <input
              type="checkbox"
              checked={!!present.selectedIds[it.id]}
              onChange={() => dispatch({ type: "toggleSelect", payload: { id: it.id } })}
            />
            <span>
              [{it.groupId}] {it.name}
            </span>

            <button
              onClick={() =>
                dispatch({ type: "update", payload: { id: it.id, patch: { done: !it.done } } })
              }
            >
              Toggle Done
            </button>

            <button onClick={() => dispatch({ type: "delete", payload: { id: it.id } })}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

你会发现 useReducer 的“爽点”在哪里

  • 组件里只做:dispatch({type:..., payload:...})

  • 所有更新规则都在 reducer:可读、可控、可扩展

  • 复杂操作(批量、撤销重做)天然适合 reducer


6)useReducer 的两条“好习惯”(很关键)

✅ (1) reducer 必须是纯函数

  • 只根据输入算输出

  • 不要在 reducer 里发请求 / 写 localStorage / 读 DOM

副作用放在组件里或 useEffect 里。

✅ (2) action 类型用常量/枚举

避免字符串拼错,也方便重构、TS 自动提示。


7)什么时候不值得用 useReducer?

  • 状态就 1~2 个字段,更新很少:用 useState 更简单

  • reducer 写太大:需要拆分 reducer 或者引入更合适的状态方案


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐