useReducer的理解与应用
三种操作,如果继续扩展成 edit / clearCompleted / reorder,用 useState 会越来越乱。动作:setField、setError、reset、submitStart/Success/Fail。动作:fetchStart / fetchSuccess / fetchError / reset。当你的状态变复杂、更新方式很多、或者你想把“怎么改状态”集中管理时,用它
useReducer 你可以把它当成 “更规范、更可控的 useState”:
当你的状态变复杂、更新方式很多、或者你想把“怎么改状态”集中管理时,用它会非常爽。
1)useReducer 是什么(核心概念)
useReducer 的核心思想来源于 Flux/Redux 架构,它遵循 “action -> reducer -> new state” 的数据流。其基本形式由三部分组成:
-
state:当前状态
-
dispatch(action):你发出一个“指令”(action)
-
reducer(state, action):一个纯函数,根据指令,接收当前状态和动作,并返回新的状态。
(state, action) => newState -
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 或者引入更合适的状态方案
更多推荐



所有评论(0)