useReducer是react hooks中提供的一个高级hooks,即便没有这些高级hooks,只用useState、useEffect和useContext,也能完成需求。但是这些高级的hooks可以让我们的代码具有更好的可读性、可维护性、可预测性。
要理解这个问题,我们得先知道reducer是什么东西。reducer是redux中一个概念,以下就是redux更新数据的一个流程。
Store, Reducer, Action是Redux的三大核心概念,同时也是useReducer的三大核心概念。redux是当前react项目中状态管理的一个主流方案,但是redux因为复杂的概念以及较高的维护成本,它的开发体验一直是一个难以解决的痛点。
1、难以维护的Action
2、复杂度无法预知的Store
3、复杂到令人绝望的Reducer
但useReducer也并没有提供相应的解决方案,useReducer只是一个小范围内的状态管理工具,它是用来在某些特殊场景下,来取代useState的,它把redux的复杂度控制在了可以接受的范围之内。
在某些场景下,useReducer
会比 useState
更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer
还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch
而不是回调函数 。
//正常使用
const [state,dispatch]=useReducer(reducer,initState);
//惰性初始化
const [state, dispatch] = useReducer(reducer, initialArg, init);
它接收一个形如 (state, action) => newState
的 reducer和一个初始值。当然这个初始值也可以通过一个函数计算出来,也就是惰性初始化。如果做惰性初始化,那么useReducer就传递了三个参数,第一个参数依旧是reducer,第三个参数是一个初始化函数,第二个参数是初始化函数要传递的参数列表。
尽管 useReducer
是扩展的 hook, 而 useState
是基本的 hook,但 useState
实际上执行的也是一个 useReducer
。这意味着 useReducer
是更原生的,你能在任何使用 useState
的地方都替换成使用 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);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
惰性初始化:
function init(initialCount) { return {count: initialCount};}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset': return init(action.payload); default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init); return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
一个登录的例子,假设没有useReducer
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
)
}
虽然代码似乎变多了,但是比起使用useState,代码更容易读懂,更容易维护了。
再来看看更接近典型Redux reducer
的例子。创建一个组件来管理购物列表,这里看还会使用另外一个 hook:useRef
。
首先,导入两个hook
import React, { useReducer, useRef } from 'react';
然后创建一个ref和reducer的组件。ref保存对表单的引用,以便咱们提取这个表单的值。
function ShoppingList() {
const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {
switch (action.type) {
// do something with the action
}
}, []);
return (
<>
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
</form>
<ul>
{items.map((item, index) => (
<li key={item.id}>
{item.name}
</li>
))}
</ul>
</>
);
}
请注意,在这种情况下,咱们的“state
”是一个数组。 咱们通过useReducer
第二个参数将它初始化为一个空数组,并从reducer
函数返回一个数组。
useRef
hook为DOM
节点创建持久引用。 调用useRef
会创建一个空的节点。它返回的对象具有current
属性,因此在上面的示例中,咱们可以使用inputRef.current
访问输入的DOM节点。 如果你熟悉React.createRef()
,则其工作原理非常相似。
但是,useRef
返回的对象不仅仅是一种保存DOM引用的方法。 它可以保存特定于此组件实例的任何值,并且它在渲染之间保持不变。
useRef
可用于创建通用实例变量,就像使用具有this.whatever = value
的React
类组件一样。 唯一的问题是,写入它会被视为“副作用”,因此咱们无法在渲染过程中更改它,需要在useEffect
hook 中才能修改。
useReducer
示例: 我们用表单来处理用户的输入,按回车提交表单。 现在来编写handleSubmit
函数,该函数主要做的是将一个项添加到列表中,以及处理reducer
中的 action
。
function ShoppingList() {
const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'add':
return [
...state,
{
id: state.length,
name: action.name
}
];
default:
return state;
}
}, []);
function handleSubmit(e) {
e.preventDefault();
dispatch({
type: 'add',
name: inputRef.current.value
});
inputRef.current.value = '';
}
return (
// ... same ...
);
}
在reducer
函数中主要判断两种情况:一种用于action.type==='add'
的情况,还有就是默认下的情况。
当action.type
为 add
时,它返回一个包含所有旧元素的新数组,以及最后的新元素。
这里有一点需要注意的是,咱们使用数组的length
作为一种自动递增的 ID 方便演示,但是对于一个真正的应用程序来说这是不可靠,因为它可能导致重复的ID和bug。(最好使用uuid这样的库,或者让服务器生成一个惟一的ID!)
当用户在输入框中按Enter键时会调用handleSubmit
函数,因此咱们需要调用preventDefault
以避免在发生这种情况时重新加载整页。 然后dispatch
派发一个 action
。
现在来看看如何从列表中删除项的。
在项目中添加一个删除,点击该按钮派发 它将发送一个 action
type === "remove"
的操作,以及要删除的项的索引。
然后咱们只需要在reducer
中处理该action
function ShoppingList() {
const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'add':
// ... same as before ...
case 'remove':
// keep every item except the one we want to remove
return state.filter((_, index) => index != action.index);
default:
return state;
}
}, []);
function handleSubmit(e) { /*...*/ }
return (
<>
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
</form>
<ul>
{items.map((item, index) => (
<li key={item.id}>
{item.name}
<button
onClick={() => dispatch({ type: 'remove', index })}
>
X
</button>
</li>
))}
</ul>
</>
);
}
useReducer
与上下文结合的好处是能够dispatch
在组件树下的任何地方调用该函数,而无需通过prop。避免需要通过组件树来跟踪它以找到回调。
const TodosDispatch = React.createContext(null);
function App() {
const [todoList, dispatch] = useReducer(toDoReducer, initialState);
return (
<TodosDispatch.Provider value={dispatch}>
...ToDoApp
</TodosDispatch.Provider>
);
}
要访问子组件中的上下文,useContext可以使用React钩子
const dispatch = useContext(TodosDispatch);
貌似这种方式可以替代redux/mobx,其实是无法替代的。这是因为每当数据发生变化,那么所有用到context的组件都会重新发生渲染,这样的话性能会很差。
state
是一个数组或者对象state
变化很复杂,经常一个操作需要修改很多state