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
  }
}
相关推荐
xiyueyezibile6 分钟前
“自迭代” Skill 的 team-pitfalls 的演进
前端·后端·ai编程
林恒smileZAZ17 分钟前
`Array(100).map(() => 1)` 为什么全为空?
前端·javascript
小巧的砖头39 分钟前
C#会重蹈覆辙吗?系列之2:反射及元数据的性能问题
开发语言·前端·c#
KaMeidebaby1 小时前
卡梅德生物技术快报|蛋白的叠氮基修饰:实操解析:核酸模板耦合蛋白的叠氮基修饰实现靶蛋白定点共价标记
前端·人工智能·物联网·算法·百度
濮水大叔1 小时前
在大型 Vue 项目中,如何有效管理复杂的状态?“散装版” vs “套装版”
前端·vue.js·typescript
玄星啊1 小时前
可修改:代码的生存底线
前端·ai编程
没落英雄2 小时前
3. DeepAgents 实战 - Memory Skills 与上下文工程
前端·人工智能·架构
Bigger2 小时前
对不起!我错怪你了,UnoCSS:一次和 AI 一起排查 DevTools 卡顿的经历
前端·css·人工智能
laowang3572 小时前
依赖故障时,日志体系需要埋哪些关键节点才能快速定位是安装失败、解析失败还是运行时缺失?
前端·javascript·vue.js·webpack·node.js
倾颜3 小时前
AI Mind 的单线程短期记忆设计:如何压缩、整理并控制当前会话上下文
前端·langchain·llm