深入学习React Hook——useReducer,并用其代替redux!

1,361 阅读4分钟

本文已参加「新人创作礼」活动,一起开启掘金创作之路。

Hook使用规则

  • 只能在函数的最外层调用Hook,不能在循环、条件判断或子函数中调用。
  • 只能在React函数组件或自定义Hook中调用Hook,不可在其他JavaScript函数中使用。

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer是useState的替代方案,它和redux非常相似,使用dispatch和payload更新数据。useReducer和useContext配合可以实现redux的绝大部分功能。 useReducer比redux优势的地方有以下几个:

  1. 官方Reat支持,bug更少且后期维护有保障。
  2. 不需要额外安装软件包,可以减小打包后的dist体积。
  3. 完善的typeScript支持,完整的代码提示,使用方便。

参数1:reducer函数

该参数必须是纯函数,即入参相同时输出结果必定相同,该纯函数必须有返回值。 reducer定义了更新state的动作类型及其对应的新state。reducer函数写法有固定范式,具体写法请看后面的案例。

参数2:initialArg初始值

initialArg可以是具体的值或对象,它定义了state的初始值。

参数3:init初始函数(可选)

  • 如果init初始函数不存在,那么使用initialArg作为state的初始值。
  • 如果init初始函数存在,那么使用init(initialArg)返回值作为初始值。
  • 该函数内可做副作用操作。

useReducer案例

代码

import React, { memo, useReducer } from "react"


const Show: React.FC<{
    count: number
}> = (props) => {
    return (
        <div>当前数字是:{props.count}</div>
    )
}

const Increase: React.FC<{ dispatch: React.Dispatch<RAction> }> = memo((props) => {
    console.log("Increase");
    return <button onClick={() => props.dispatch({ type: "add" })}> + </button>;
});

const Decrease: React.FC<{ dispatch: React.Dispatch<RAction> }> = memo((props) => {
    console.log("Decrease");
    return <button onClick={() => props.dispatch({ type: "sub" })}> - </button>;
});

const Reset: React.FC<{ dispatch: React.Dispatch<RAction> }> = memo((props) => {
    console.log("Reset");
    const initialValue = 8
    return <button onClick={() => props.dispatch({ type: "reset", payload: initialValue })}> 重置 </button>;
});

type RState = {
    count: number
}
type RAction = {
    type: "add" | "sub" | "reset"  // 操作类型
    payload?: number // 操作数
}

const reducer = (state: RState, action: RAction) => {
    switch (action.type) {
        case "add": return { count: state.count + 1 }
        case "sub": return { count: state.count > 0 ? state.count - 1 : 0 }
        case "reset": return { count: action.payload ? action.payload : 0 }
    }
}

const App = () => {
    const [state, dispatch] = useReducer(reducer, { count: 0 })
    return (
        <>
            <Show count={state.count} />
            <Increase dispatch={dispatch} />
            <Decrease dispatch={dispatch} />
            <Reset dispatch={dispatch} />
        </>
    )
}

export default App

在线体验

讲解

reducer函数

  • 参数1:之前的state。
  • 参数2:是一个包含type和payload这2个属性的对象,type中定义了dispatch操作类型,payload(可缺省)定义了dispatch操作数。
  • 返回值:完整的、全新的state。react会使用reducer函数返回值来更新state。

dispatch函数

  • 参数是一个包含type和payload两个属性的对象。type中定义了dispatch操作类型,payload(可缺省)定义了dispatch操作数。
  • dispatch函数的标识是稳定的,不会在组件重新渲染时改变。不要把dispatch加入useEffect、useCallback等hook的依赖项。

useReducer和useContext搭配使用案例

useReducer和useContext配合可以实现redux的绝大部分功能,而且代码量比redux还少一些。建议对useContext还不熟悉的先关注我,看看我写的《用typescript写React Context案例详细讲解》这篇博客,熟悉useContext以后再往下看。

代码

为了结构清楚,在这里将reducer部分、context部分都做了拆分。

reducer.tsx

export type RState = {
    count: number
}
export type RAction = {
    type: "add" | "sub" | "reset"  // 操作类型
    payload?: number // 操作数
}

export const reducer = (state: RState, action: RAction) => {
    switch (action.type) {
        case "add": return { count: state.count + 1 }
        case "sub": return { count: state.count > 0 ? state.count - 1 : 0 }
        case "reset": return { count: action.payload ? action.payload : 0 }
    }
}

讲解

这部分和之前的useReducer案例相关内容几乎一致。

context.tsx

import React, { createContext, useReducer } from "react";
import { RState, RAction, reducer } from './reducer'

// 创建context,约定数据类型,设置初始值
export const Context = createContext<{
    state: RState;
    dispatch: React.Dispatch<RAction>;
} | null>(null);

// ContextProvide组件
const ContextProvide: React.FC<{
    children: React.ReactNode[];
}> = (props) => {
    const [state, dispatch] = useReducer(reducer, { count: 0 })

    return (
        <Context.Provider value={{ state, dispatch }}>
            {props.children}
        </Context.Provider>
    );
};

export default ContextProvide;

讲解

这里创建了Context容器,容器里放了useReducer的state和dispatch。

App.tsx

import React, { useContext,memo } from "react"
import ContextProvide, { Context } from './context'
import { RAction } from "./reducer"


const Show = () => {
    const { state } = useContext(Context)!
    return (
        <div>当前数字是:{state.count}</div>
    )
}

const UI: React.FC<{ dispatch: React.Dispatch<RAction> }> = memo(
    (props) => {
      console.log("Increase");
      return <button onClick={() => props.dispatch({ type: "add" })}> + </button>;
    }
);
  
const Increase = () => {
    const { dispatch } = useContext(Context)!
    return <UI dispatch={dispatch}/>
}


const Decrease = () => {
    console.log("Decrease")
    const { dispatch } = useContext(Context)!
    return <button onClick={() => dispatch({ type: "sub" })}> - </button>
}

const Reset = memo(() => {
    console.log("Reset")
    const { dispatch } = useContext(Context)!
    const initialValue = 8
    return <button onClick={() => dispatch({ type: "reset", payload: initialValue })}> 重置 </button>
})


const App = () => {
    return (
        <ContextProvide>
            <Show />
            <Increase />
            <Decrease />
            <Reset />
        </ContextProvide>
    )
}

export default App

讲解

  • 这里是使用useContext和useReducer后的状态,在这state和dispatch不再通过props传递,而是通过context传递。
  • 无法用memo对通过context传递的state、dispatch的组件进行性能优化。请认真观察console.log的打印日志。Reset组件被包装在memo中,但是仍然存在重复渲染的情况。
  • 使用useContext、useReducer时做性能优化的方式请参考Increase组件中使用的方式。在该组件中接收state、dispatch,然后将其通过props传递给UI组件,用memo对UI组件进行包装,之后Increase就不会出现重复渲染的情况。

在线体验