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
  }
}
相关推荐
dly_blog1 小时前
Vue 响应式陷阱与解决方案(第19节)
前端·javascript·vue.js
消失的旧时光-19431 小时前
401 自动刷新 Token 的完整架构设计(Dio 实战版)
开发语言·前端·javascript
console.log('npc')2 小时前
Table,vue3在父组件调用子组件columns列的方法展示弹窗文件预览效果
前端·javascript·vue.js
用户47949283569152 小时前
React Hooks 的“天条”:为啥绝对不能写在 if 语句里?
前端·react.js
我命由我123452 小时前
SVG - SVG 引入(SVG 概述、SVG 基本使用、SVG 使用 CSS、SVG 使用 JavaScript、SVG 实例实操)
开发语言·前端·javascript·css·学习·ecmascript·学习方法
用户47949283569153 小时前
给客户做私有化部署,我是如何优雅搞定 NPM 依赖管理的?
前端·后端·程序员
C_心欲无痕3 小时前
vue3 - markRaw标记为非响应式对象
前端·javascript·vue.js
qingyun9893 小时前
深度优先遍历:JavaScript递归查找树形数据结构中的节点标签
前端·javascript·数据结构
熬夜敲代码的小N3 小时前
Vue (Official)重磅更新!Vue Language Tools 3.2功能一览!
前端·javascript·vue.js
90后的晨仔3 小时前
用 Python 脚本一键重命名序列帧图片的名称
前端