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
  }
}
相关推荐
ze_juejin10 分钟前
Fetch API 详解
前端
用户669820611298219 分钟前
js今日理解 blob和arrayBuffer 二进制数据
前端·javascript
想想肿子会怎么做21 分钟前
Flutter 环境安装
前端·flutter
断竿散人22 分钟前
Node 版本管理工具全指南
前端·node.js
转转技术团队23 分钟前
「快递包裹」视角详解OSI七层模型
前端·面试
1024小神28 分钟前
Ant Design这个日期选择组件最大值最小值的坑
前端·javascript
卸任29 分钟前
Electron自制翻译工具:自动更新
前端·react.js·electron
安禅不必须山水30 分钟前
Express+Vercel+Github部署自己的Mock服务
前端
哈撒Ki33 分钟前
快速入门zod4
前端·node.js
用户游民1 小时前
Flutter 项目热更新方案对比与实现指南
前端