本文已参加「新人创作礼」活动,一起开启掘金创作之路。
Hook使用规则
- 只能在函数的最外层调用Hook,不能在循环、条件判断或子函数中调用。
- 只能在React函数组件或自定义Hook中调用Hook,不可在其他JavaScript函数中使用。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer是useState的替代方案,它和redux非常相似,使用dispatch和payload更新数据。useReducer和useContext配合可以实现redux的绝大部分功能。 useReducer比redux优势的地方有以下几个:
- 官方Reat支持,bug更少且后期维护有保障。
- 不需要额外安装软件包,可以减小打包后的dist体积。
- 完善的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就不会出现重复渲染的情况。