React 状态管理之useReducer

useReducer

当状态更新逻辑较复杂时可以考虑使用 useReducer。useReducer 可以同时更新多个状态,而且能把对状态的修改从组件中独立出来。相比于 useState,useReducer 可以更好的描述"如何更新状态"。

useReducer 的语法格式

useReducer 的基础语法如下:

js 复制代码
const [state, dispatch] = useReducer(reducer, initState, initAction?)

其中:

  1. reducer 是一个函数,类似于 (prevState, action) => newState。形参 prevState 表示旧状态,形参 action 表示本次的行为,返回值 newState 表示新状态。
  2. initState 表示初始状态、默认值。
  3. initAction 是进行状态初始化时候的处理函数,可选的。initAction 函数会把initState 传递给 initAction 函数进行处理,返回值会被当做初始状态。
  4. 返回值 state 是状态值。dispatch 是更新 state 的方法,接收 action 作为参数,只需要调用dispatch(action) 方法传入的 action 即可更新 state。

useReducer 的基础用法

  1. 定义组件的基础结构
tsx 复制代码
import React from 'react'

export const Father: React.FC = () => {
  return (
    <div>
      <button>修改</button>
      <div className="father">
        <Son1 />
        <Son2 />
      </div>
    </div>
  )
}

const Son1: React.FC = () => {
  return <div className="son1"></div>
}

const Son2: React.FC = () => {
  return <div className="son2"></div>
}
  1. 定义 useReducer 的基础结构
js 复制代码
import React, { useReducer } from 'react'
// 定义初始数据:
const defaultState = { name: 'kkx', age: 28 }
// 定义 reducer 函数,它的作用是:根据旧状态,进行一系列处理,最终返回新状态:
const reducer = (prevState) => {
  return prevState
}

export const Father: React.FC = () => {
  const [state] = useReducer(reducer, defaultState)
  return (
    <div>
      <button>修改</button>
      <div className="father">
        <Son1 />
        <Son2 />
      </div>
    </div>
  )
}

// 使用 initAction 处理初始数据 定义名为 initAction 的处理函数,如果初始数据中的 age 为小数、负数、或 0 时,对 age 进行非法值的处理:

const initAction = (initState: UserType) => {
  // 把 return 的对象,作为 useReducer 的初始值
  return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 }
}
  1. 点击按钮修改 name 的值

3.1 🚩错误示范

tsx 复制代码
export const Father: React.FC = () => {
  const [state] = useReducer(reducer, defaultState, initAction)
  const onChangeName = () => {
    // 🚩 错误行为:直接修改对象 不能直接修改 state 的值
    // 因为存储在 useReducer 中的数据都是"不可变"的!
    state.name = 'escook'
  }
  return (
    <div>
      <button onClick={onChangeName}>修改</button>
    </div>
  )
}

3.2 ✅正确的操作

tsx 复制代码
const [state, dispatch] = useReducer(reducer, defaultState, initAction)

// 调用 dispatch() 函数,从而触发 reducer 函数的重新计算:
const onChangeName = () => {
  dispatch()
}

3.3 调用 dispatch 传递参数给 reducer 在 Father 父组件按钮的点击事件处理函数 onChangeName 中,调用 dispatch() 函数并把参数传递给 reducer 的

js 复制代码
const onChangeName = () => {
  // 注意:参数的格式为 { type, payload? }
  // type 的值是一个唯一的标识符,用来指定本次操作的类型
  // payload 是本次操作需要用到的数据,为可选参数。
  dispatch({ type: 'UPDATE_NAME', payload: 'XXX' })
}

修改 reducer 函数的形参,添加名为 action 的第 2 个形参,用来接收 dispatch 传递过来的数据:

js 复制代码
const reducer = (prevState: UserType, action) => {
  // {type: 'UPDATE_NAME', payload: 'fff'}
  return prevState
}

在 reducer 中,根据接收到的 action.type 标识符,决定进行怎样的更新操作,最终 return 一个计算好的新状态。示例代码如下:

js 复制代码
type ActionType = { type: 'UPDATE_NAME'; payload: string }
const reducer = (prevState: UserType, action: ActionType) => {
  console.log('reducer 函数', action)

  switch (action.type) {
    // 如果标识符是字符串 'UPDATE_NAME',则把用户名更新成 action.payload 的值
    // 最后,一定要返回一个新状态,因为 useReducer 中每一次的状态都是"不可变的"
    case 'UPDATE_NAME':
      // ✅ 创建一个新的对象 不要忘记复制之前的属性!...prevState
      return { ...prevState, name: action.payload }
    // 如果没有匹配到任何操作,则默认返回上一次的旧状态
    default:
      return prevState
  }
}
  1. 完整的reducer案例
tsx 复制代码
type ActionType = { type: 'UPDATE_NAME'; payload: string } | { type: 'INCREMENT'; payload: number } | { type: 'RESET' }
const reducer = (prevState: UserType, action: ActionType) => {
  switch (action.type) {
    case 'UPDATE_NAME':
      return { ...prevState, name: action.payload }
    case 'INCREMENT':
      return { ...prevState, age: prevState.age + action.payload }
    case 'DECREMENT':
      return { ...prevState, age: prevState.age - action.payload }
    case 'RESET':
      return defaultState
    default:
      return prevState
  }
}

export const Father: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, defaultState, initAction)

  const onChangeName = () => {
    dispatch({ type: 'UPDATE_NAME', payload: 'xxxx' })
  }

  return (
    <div>
      <button onClick={onChangeName}>修改</button>
      <div className="father">
        <Son1 {...state} dispatch={dispatch} />
        <Son2 {...state} dispatch={dispatch} />
      </div>
    </div>
  )
}

const Son1: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {
  const { dispatch, ...user } = props

  const add = () => dispatch({ type: 'INCREMENT', payload: 1 })

  return (
    <div className="son1">
      <button onClick={add}>加一</button>
    </div>
  )
}

const Son2: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {
  const { dispatch, ...user } = props
  const sub = () => dispatch({ type: 'DECREMENT', payload: 5 })

  return (
    <div className="son2">
      <button onClick={sub}>减五</button>
    </div>
  )
}
const GrandSon: React.FC<{ dispatch: React.Dispatch<ActionType> }> = (props) => {
  const reset = () => props.dispatch({ type: 'RESET' })
  return (
    <>
      <button onClick={reset}>重置</button>
    </>
  )
}
  1. 使用 Immer 编写更简洁的 reducer 更新逻辑
bash 复制代码
npm install immer use-immer -S
  • 从 use-immer 中导入 useImmerReducer 函数,并替换掉 useReducer 函数的调用:
ts 复制代码
// 1. 导入 useImmerReducer
import { useImmerReducer } from 'use-immer'
export const Father: React.FC = () => {
  // 2. 把 useReducer() 的调用替换成 useImmerReducer()
  const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction)
}
  • 修改 reducer 函数中的业务逻辑,case 代码块中不再需要 return 不可变的新对象了,只需要在 prevState 上进行修改即可。Immer 内部会复制并返回新对象。
ts 复制代码
const reducer = (prevState: UserType, action: ActionType) => {
  switch (action.type) {
    case 'UPDATE_NAME':
      prevState.name = action.payload
      break
    case 'INCREMENT':
      prevState.age += action.payload
      break
    case 'DECREMENT':
      prevState.age -= action.payload
      break
    case 'RESET':
      return defaultState
    default:
      return prevState
  }
}
相关推荐
benbenAItalk7 小时前
数字人口播视频的批量生产实践:素材规范、任务编排与质量验收
java·前端·音视频
2601_962885728 小时前
如何用 Python 扫描 A 股跳空缺口并统计缺口回补概率?
java·前端·python
西瓜太郎12348 小时前
Claude Code、Codex CLI、Gemini CLI 能否共用一枚 Key?先看协议选择矩阵
前端·api 网关·claude code·gemini cli·codex cli
陈随易10 小时前
在Finch用了62亿词元,我认为这是新一代Agent工具之神
前端·人工智能·后端
水域安全老周10 小时前
水趣钓鱼救生衣专利拆解:两级锁紧如何解决落水人衣分离
java·前端·网络
计算机魔术师11 小时前
Anthropic CEO突然喊踩刹车,OpenAI罕见力挺:AI这辆车不能只踩油门了
前端
wing9811 小时前
从codex转战workbuddy使用一周的感受
前端·人工智能·后端
EatFan11 小时前
Java接入支付宝 JSAPI 支付保姆教程(二):流程讲解与前后端代码讲解
前端·spring boot·后端·微信小程序·小程序·uni-app
梦想平凡12 小时前
百游棋牌源代码开发搭建教程(五):房间创建、座位分配与请求幂等实现
前端·javascript·数据库·源代码管理
IT_陈寒12 小时前
Redis的Set操作居然能把我的服务整挂了?
前端·人工智能·后端