useReducer
当状态更新逻辑较复杂时可以考虑使用 useReducer。useReducer 可以同时更新多个状态,而且能把对状态的修改从组件中独立出来。相比于 useState,useReducer 可以更好的描述"如何更新状态"。
useReducer 的语法格式
useReducer 的基础语法如下:
js
const [state, dispatch] = useReducer(reducer, initState, initAction?)
其中:
- reducer 是一个函数,类似于 (prevState, action) => newState。形参 prevState 表示旧状态,形参 action 表示本次的行为,返回值 newState 表示新状态。
- initState 表示初始状态、默认值。
- initAction 是进行状态初始化时候的处理函数,可选的。initAction 函数会把initState 传递给 initAction 函数进行处理,返回值会被当做初始状态。
- 返回值 state 是状态值。dispatch 是更新 state 的方法,接收 action 作为参数,只需要调用dispatch(action) 方法传入的 action 即可更新 state。
useReducer 的基础用法
- 定义组件的基础结构
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>
}
- 定义 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 }
}
- 点击按钮修改 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
}
}
- 完整的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>
</>
)
}
- 使用 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
}
}