欢迎光临
我们一直在努力

useReducer的理解与应用

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)
    未经允许不得转载:171主机测评 » useReducer的理解与应用
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址