useReducer 是什么
使用方法
useReducer
是 useState
的替代方案。可以通过下面的源码,看下 useState
和 useReducer
传参和返回值的类型。
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
export function useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArg, init);
}
const [state, setState] = useState(initialState);
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer
接收三个参数:
reducer
—— 是函数(state, action) => newState
state
是当前最新的状态值,action
用于告诉reducer
当前执行的操作,reducer
会根据不同操作执行不同逻辑去更新state
reducer
是一个纯函数,没有任何 UI 和副作用
initialArg
—— 是指定的初始state
init
—— 是函数(initialArg) => initialState
- 这个参数是可选的,如果传入了
init
,初始state
就是init(initialArg)
的返回结果
- 这个参数是可选的,如果传入了
useReducer
返回一个数组:
- 第一项是
state
- 第二项是
dispatch
,是函数(action) => void
- 通过调用
dispatch
传入action
,来告诉reducer
执行什么操作,然后更新state
dispatch
不会在组件重新渲染时改变
- 通过调用
可以通过下面这个计数器的栗子,直观地看下怎么使用 useReducer
。
const initializer = (initialValue) => {
return {
count: initialValue.count + 10,
};
};
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState, initializer);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
因为 useReducer
传入了第三个参数 initializer
,所以初始 state
不是 0,而是计算后的 10。
和 Redux 的关系
Store
, Reducer
, Action
是 Redux
的三核心概念,同时也是 useReducer
的三大核心概念。
通过调用 useReducer
返回的 dispatch
,传入 action
来告诉 reducer
要执行的操作,然后 reducer
根据 action
更新 store
,然后响应到 view
层。
useState vs useReducer
在 state
更新方面,使用 useReducer
和使用 useState
在 setState
传入回调函数时的效果一样,即 state
的更新是异步的,并且不会合并。
🌰
useState
↓
function Counter() {
const [count, setCount] = useState(0);
console.log("render");
return (
<div>
count: {count}
<button
onClick={() => {
setCount((c) => c + 1);
console.log("count", count);
setCount((c) => c + 1);
console.log("count", count);
}}
>
+
</button>
</div>
);
}
useReducer
↓
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
console.log("render");
return (
<div>
count: {state.count}
<button
onClick={() => {
dispatch({ type: "increment" });
console.log("count", state.count);
dispatch({ type: "increment" });
console.log("count", state.count);
}}
>
+
</button>
</div>
);
}
点击 + 后,都是这个结果 ↓
useState
是细粒度的状态管理。本质来说,useState
就是预置了 reducer
的 useReducer
,即 每次调用 setState
,相当于调用 dispatch
让 reducer
把传入值赋值到对应的 state
。
reducer
体现的编程思想,类似于设计模式中的状态模式。状态模式是,当不同的状态有不同的行为,并且在代码中存在大量和状态相关的条件语句的时候,把具体的状态类抽象出来,最后只用委托状态类调用其中的方法即可,不用关心具体的实现。reducer
是,每一种状态对应一组数据,状态变化的时候就更新对应组的数据,使用时也不用关心具体数据。当一种状态对应多个数据变化,比如一个操作需要更新多个 state
的时候,reducer
就能聚合了这种变化。
useReducer 的使用场景
当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能需要用 useReducer
去替换它们。
场景一:需要同时更新多个状态时
🌰
使用 useState
时:
function LoginPage() {
const [name, setName] = useState(""); // 用户名
const [pwd, setPwd] = useState(""); // 密码
const [isLoading, setIsLoading] = useState(false); // 是否展示loading,发送请求中
const [error, setError] = useState(""); // 错误信息
const [isLoggedIn, setIsLoggedIn] = useState(false); // 是否登录
const login = (event) => {
event.preventDefault();
setError("");
setIsLoading(true);
login({ name, pwd })
.then(() => {
setIsLoggedIn(true);
setIsLoading(false);
})
.catch((error) => {
// 登录失败: 显示错误信息、清空输入框用户名、密码、清除loading标识
setError(error.message);
setName("");
setPwd("");
setIsLoading(false);
});
};
return (
// 返回页面 JSX Element
);
}
使用 useReducer
时:
const initState = {
name: "",
pwd: "",
isLoading: false,
error: "",
isLoggedIn: false,
};
function loginReducer(state, action) {
switch (action.type) {
case "login":
return {
...state,
isLoading: true,
error: "",
};
case "success":
return {
...state,
isLoggedIn: true,
isLoading: false,
};
case "error":
return {
...state,
error: action.payload.error,
name: "",
pwd: "",
isLoading: false,
};
default:
return state;
}
}
function LoginPage() {
const [state, dispatch] = useReducer(loginReducer, initState);
const { name, pwd, isLoading, error, isLoggedIn } = state;
const login = (event) => {
event.preventDefault();
dispatch({ type: "login" });
login({ name, pwd })
.then(() => {
dispatch({ type: "success" });
})
.catch((error) => {
dispatch({
type: "error",
payload: { error: error.message },
});
});
};
return (
// 返回页面 JSX Element
)
}
场景二:解耦,移除不必需的依赖,避免不必要的 effect 调用
🌰
使用 useState
时:
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount((c) => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={(e) => setStep(Number(e.target.value))} />
</>
);
}
使用 useReducer
时:
const initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === "tick") {
return { count: count + step, step };
} else if (action.type === "step") {
return { count, step: action.step };
} else {
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" });
}, 1000);
return () => clearInterval(id);
}, []);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={(e) => setStep(Number(e.target.value))} />
</>
);
}
使用场景总结
可以在一个自定义 hook 或组件中同时使用多个 useState
或 useReducer
。
按照使用作用域分离 state
,如果它们一起更新,最好将其放在 reducer
中;如果某项 state
与该自定义 hook 或组件中的其他 state
来说非常独立,那么将其与其他 state
绑定在一起只会给 reducer
增加不必要的复杂性,最好就让它保持 useState
的形式。