我搞懂了React 的 useReducer

useReducer 是除useState之外另一个与状态管理相关的 hook,对于熟悉 Redux 的工程师而言,理解 useReducer 将变得简单,在 React 内部,useState 由 useReducer 实现。

useReducer 的类型定义

useReducer 的类型定义如下:

function useReducer<R extends ReducerWithoutAction<any>, I>(
    reducer: R,
    initializerArg: I,
    initializer: (arg: I) => ReducerStateWithoutAction<R>
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];

function useReducer<R extends ReducerWithoutAction<any>>(
    reducer: R,
    initializerArg: ReducerStateWithoutAction<R>,
    initializer?: undefined
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];

function useReducer<R extends Reducer<any, any>, I>(
    reducer: R,
    initializerArg: I & ReducerState<R>,
    initializer: (arg: I & ReducerState<R>) => ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

function useReducer<R extends Reducer<any, any>, I>(
    reducer: R,
    initializerArg: I,
    initializer: (arg: I) => ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

function useReducer<R extends Reducer<any, any>>(
    reducer: R,
    initialState: ReducerState<R>,
    initializer?: undefined
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

useReducer 的类型定义很复杂,一共有 5 个重载,总体而言,它最多接受3个参数,第 1 个参数是一个用于更新状态的函数,之后将它称为 reducer。第 3 个参数非必填,如果不存在第 3 个参数,那么第 2 个参数将作为状态的初始值;如果存在第 3 个参数,那么它必须是函数,此时第 2 个参数被传递给该函数用于计算状态的初始值,该函数只在组件初始渲染时执行一次。useReducer 的返回值是一个长度为 2 的数组,数组的第 1 个位置是状态值,第 2 个位置是一个用于触发状态更新的函数,将它记为dispatch,调用 dispatch 将导致 reducer 被调用。接下来通过计数器 demo 对比 useState 和useReducer 用法上的差异。

用 useState 实现计数器

用 useState 实现计数器,代码如下:

function UseStateCounterDemo() {
    const [value, setValue] = useState<number>(0)
    const [step, setStep] = useState<number>(1)

    const onChangeStep = (ev: React.ChangeEvent<HTMLInputElement>) => {
        setStep(Number(ev.target.value))
    }

    return (
        <div>
            <h2>用useState实现计数器</h2>
            count: {count}; 
            step: <input type='number' value={step} onChange={onChangeStep}/>
            <button onClick={() => setValue(value + step)}>加</button>
            <button onClick={() => setValue(value - step)}>减</button>
            <button onClick={() => {setValue(0); setStep(1)}}>重置</button>
        </div>
    )
}

上述代码很简单,它使用 useState 定义了两个状态,分别表示计数器的值和加减步数,在过去的文章里已对 useState 做过详细的介绍,这里不再赘述,下面详细介绍用 useReducer 实现计数器。

用 useReducer 实现计数器

定义 reducer

使用 useReducer hook 离不开reducer,它是一个函数,用于更新 useReducer 返回的状态,代码如下:

const initArg: Counter = {value: 0, step: 1}

// 它用于更新state
function reducer(prevState: Counter, action: Action): Counter  {
    switch (action.type) {
        case 'increment':
            return {
                ...prevState,
                value: prevState.value + prevState.step
            }
        case 'decrement':
            return {
                ...prevState,
                value: prevState.value - prevState.step
            }
        case 'reset':
            return initArg
        case 'changeStep':
            return {
                ...prevState,
                step: action.value || initArg.value
            }
        default:
            throw new Error();
    }
}

reducer 用于更新状态,计数器 demo 有两个状态,分别是计数器的当前值和它的加减步数,这两个状态密切相关,这里用一个 TS 接口去描述它,代码如下:

interface Counter {
   // 计数器的当前值
    value: number
   // 计数器的加减步数
    step: number
}

计数器有 3 种操作,分别是加、减、重置和修改步数,这里用 TS 接口描述这些行为,代码如下:

interface Action {
    type: 'increment' | 'decrement' | 'reset'|'changeStep',
    value?: number
}

在组件中使用 useReducer

// 计数器的初始值
const initArg: Counter = {value: 0, step: 1}

function UseReducerCounterDemo() {
    // 使用上一步定义 reducer
    const [counter, dispatch] = useReducer(reducer, initArg)

    const onChangeStep = (ev: React.ChangeEvent<HTMLInputElement>) => {
        dispatch({type: 'changeStep',value: Number(ev.target.value)})
    }

    return (
        <div>
            <h2>用useReducer实现计数器</h2>
            count: {counter.value}; 
            step: <input type='number' value={counter.step} onChange={onChangeStep}/>
            <button onClick={() => dispatch({type: 'increment'})}>加</button>
            <button onClick={() => dispatch({type: 'decrement'})}>减</button>
            <button onClick={() => dispatch({type: 'reset'})}>重置</button>
        </div>
    )
}

只考虑代码量,读者应该都会认为useState比useReducer更简洁。仔细观察可以发现,上述计数器除了有value还有step,step对value有影响,UseStateCounterDemo组件将它们零散地保存在不同的状态中,UseStateCounterDemo组件将它们关联在同一个状态中,内聚性更高。useState与useReducer没有优劣之外,它们有各自适用的场景,这里有如下建议:

  1. 当状态是一个拥有很多属性的复杂对象,并且状态更新涉及复杂的逻辑时,推荐使用useReducer。
  2. 当某个状态的更新受另一个状态影响时,推荐使用 useReducer将它们放在一起。
  3. 当状态只是单独的基本数据类型时,推荐使用 useState。

介绍 useEffect时曾强调,为了在 effect 中拿到状态最新的值,必须给 effect 设置正确地依赖项。在 useEffect 中使用 useReducer 返回的 dispatch 能让 effect 自给自足,减少依赖项。示例代码如下:

function DispatchDemo(props: {step: number}) {
    function reducer(value: number) {
	// 始终能访问到最新的 step 
        return props.step + value
    }
    const [value, dispatch] = useReducer(reducer, 0)

    useEffect(() => {
        document.body.addEventListener('click', dispatch)
        return () => {
            document.body.removeEventListener('click', dispatch)
        }
    }, [])

    return // todo
}

上述代码 useEffect 的第二个参数为空数组,这意味着 effect 只在组件初始渲染时执行。由于React 会让 dispatch 在组件的每次渲染中保持唯一的引用,所以 dispatch 不必出现在effect的依赖中,此特性与 ref 类似。虽然 dispatch 的引用保持不变,但它能调用组件本次渲染时的reducer,在 reducer 作用域将得到最新的 state 和 props。

推荐阅读

  1. 我搞懂了 React 的 useState 和 useEffect

原文链接:https://juejin.cn/post/7244876679041794105 作者:何遇er

(0)
上一篇 2023年6月16日 上午10:15
下一篇 2023年6月16日 上午10:25

相关推荐

发表回复

登录后才能评论