useReducer基础用法
useReducer的作用是“勾住”某些自定义数据对应的dispatch所引发的数据更改事件。useReducer可以替代useState,实现更为复杂逻辑的数据修改。
useReducer是来解决什么问题的?
答:useReducer是useState的升级版(实际上应该是原始版),可以实现复杂逻辑修改,而不是像useState那样只是直接赋值修改。
补充说明:
1、在React源码中,实际上useState就是由useReducer实现的,所以useReducer准确来说是useState的原始版。
2、无论哪一个Hook函数,本质上都是通过事件驱动来实现视图层更新的。
useReducer源码
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);
}useReducer基本用法
useReducer(reducer,initialValue)函数通常传入2个参数,第1个参数为我们定义的一个“由dispatch引发的数据修改处理函数”,第2个参数为自定义数据的默认值,useReducer函数会返回自定义变量的引用和该自定义变量对应的“dispatch”。
请注意,当你看到了dispatch,肯定想到了原生JS中的EventEmitter,事实上React Hook帮我们做了底层的事件驱动处理,我们拿到的dispatch以及“事件处理函数”reducer,都时被React Hook 封装过后的,并不是真正的抛出和事件处理函数。
但是为了更容易让你理解,本文依然会在讲解useReducer时使用到“事件抛出、事件处理函数”等文字。
如果你了解事件驱动,使用过EventEmitter,或者你使用过Redux,那么你会很容易理解useReducer的用法。
代码形式
import React, { useReducer } from 'react'; //引入useReducer
//定义好“事件处理函数” reducer
function reducer(state, action) {
switch (action) {
case 'xx':
return xxxx;
case 'xx':
return xxxx;
default:
return xxxx;
}
}
function Component(){
//声明一个变量xxx,以及对应修改xxx的dispatch
//将事件处理函数reducer和默认值initialValue作为参数传递给useReducer
const [xxx, dispatch] = useReducer(reducer, initialValue);
//若想获取xxx的值,直接使用xxx即可
//若想修改xxx的值,通过dispatch来修改
dispatch('xx');
}请注意,上述代码中的action只是最基础的字符串形式,事实上action可以是多属性的object,这样可以自定义更多属性和更多参数值
例如 action 可以是 {type:'xx',param:xxx}
拆解说明
- 具体讲解已在上面示例代码中做了多项注释,此处不再重复;
'reducer'补充说明
reducer英文单词本身意思是“减速器、还原剂”,但是本文中一直把reducer称呼为“事件处理函数”,但事实上reducer确实扮演一个事件处理函数。
千万不要把useReducer中的 reducer 和 原生JS中的Array.prototype.reduce()``弄混淆,他们两个只是刚好都使用了这个reduce单词而已,两者本身没有任何内在关联。
'xxx'补充说明
假设我们定义的变量名为xxx,那么只能通过dispatch来修改xxx,不要尝试通过 xxx = newValue 这种形式直接修改变量的值,React 不允许这样做。
'dispatch'补充说明
再次强调,dispacth并不是真正的Event.dispatch,但是你完全可以把它当成Event.dispatch来理解,只不过useReducer中的dispacth(xxx)函数抛出内容不是event,而是一个包含修改信息的对象,该对象不仅可以是字符串,还可以是复杂对象。
'initialValue'补充说明
initialValue是我们自定义变量的默认值,该值可以是简单类型(number、string),也可以是复杂类型(object、array)。
推荐建议:即使该值是简单类型,也建议单独定义出来而不是直接将值写在useReducer函数中,因为单独定义可以让我们更加清晰读懂数据结构,尤其是initialValue为复杂类型时。
import React, { useReducer } from 'react';
function reducer(state,action){
switch(action){
case 'add':
return state + 1;
case 'sub':
return state - 1;
case 'mul':
return state * 2;
default:
console.log('what?');
return state;
}
}
function CountComponent() {
const [count, dispatch] = useReducer(reducer,0);
return <div>
{count}
<button onClick={() => {dispatch('add')}} >add</button>
<button onClick={() => {dispatch('sub')}} >sub</button>
<button onClick={() => {dispatch('mul')}} >mul</button>
</div>;
}
export default CountComponent;useReducer高级用法
使用useReducer来管理复杂类型的数据
举例,若某组件内通过ajax请求数据,获取最新一条站内短信文字,需要组件显示整个ajax过程及结果:
当ajax开始请求时,界面显示“loading...”;
当ajax请求发生错误时,界面显示“wrong!”;
当ajax请求成功获取数据时,界面显示获取到的数据内容;
如果我们使用useState来实现上述功能,伪代码如下:
function Component() {
const [loading,setLoading] = useState(true); //是否ajax请求中,默认为true
const [result,setResult] = useState(''); //请求数据内容,默认为''
const [error,setError] = useState(false); //请求是否发生错误,默认为false
{
//ajax请求成功
setLoading(false);
setResult('You have a good news!');//请注意,这是一行伪代码,只是为了演示,并不是真正ajax获取的结果
setError(false);
//ajax请求错误
setLoading(false);
setError(true);
}
return <div>
{loading ? 'loading...' : result}
{error ? 'wrong!' : null}
</div>
}如果我们使用useReducer来实现,则可将上述3个变量都放在我们定义的变量state中,伪代码如下:
const initralData = {loading: true,result: '',error: false};
const reducer = (state, action) => {
switch (action.type) {
case 'succes':
return {loading:false,result:action.res,error:false}
case 'error':
return {loading:false,error:true}
}
}
function Component() {
const [state, dispatch] = useReducer(reducer, initralData);
{
//ajax请求成功
dispatch({type:'succes',res:'You have a good news!'});
//ajax请求错误
dispatch({type:'error'});
}
return <div>
{state.loading ? 'loading...' : state.result}
{state.error ? 'wrong!' : null}
</div>
}你可能会有疑问?
为什么看上去使用useReducer后代码变得更多?
答:因为使用useReducer,我们将修改数据拆分为2个部分,即“抛出修改事件和事件修改处理函数”。虽然代码增多了,但是逻辑更加清晰。为什么不使用useState,同时把它对应的变量也做成一个obj,就像useReducer的initralData那种?
答:单纯从1次ajax请求很难看出使用useState或useReducer的差异,但是试想一下多次且ajax返回值在结构类型上容易发生变更,那么使用useReducer这种更加利于代码阅读、功能扩展。
使用useContext和useReducer实现操作全局共享数据
实现原理
用 useContext 实现“获取全局数据”
用 useReducer 实现“修改全局数据”
实现思路
1、用React.createContext()定义一个全局数据对象;
2、在父组件中用 useReducer 定义全局变量xx和负责抛出修改事件的dispatch;
3、在父组件之外,定义负责具体修改全局变量的处理函数reducer,根据修改xx事件类型和参数,执行修改xx的值;
4、在父组件中用<XxxContext.Provider value=>标签把 全局共享数据和负责抛出修改xx的dispatch 暴露给子组件;
5、在子组件中用 useContext获取全局变量;
6、在子组件中用 xxContext.dispatch 去抛出修改xx的事件,携带修改事件类型和参数;
global-context.ts
import React from "react";
const GlobalContext = React.createContext({ age: 0, dispatch: () => { } });
export default GlobalContextAppComponent.tsx
//定义好“事件处理函数” reducer
function reducer(state, action) {
switch (action) {
case "add":
return state + 1;
case "sub":
return state - 1;
default:
return 0;
}
}
function AppComponent() {
const [count, dispatch] = useReducer(reducer, 22);
//如果不添加value,那么子组件获取到的共享数据value值是React.createContext(defaultValues)中的默认值defaultValues
return <div>
<GlobalContext.Provider value={{ count, dispatch }}>
<ComponentA />
<ComponentB />
<ComponentC />
</GlobalContext.Provider>
</div >
}ComponentA.tsx
import { useContext } from 'react';
import GlobalContext from '../global-context.ts';
export default function ComponentA() {
const countContext = useContext(GlobalContext);
return <div>
<h1>ComponentA - count={countContext.count}</h1
<button onClick={() => { countContext.dispatch("add") }}>add</button>
<button onClick={() => { countContext.dispatch("sub") }}>sub</button>
</div>
}为什么不使用Redux?
这个问题以前提出过,现在可以明确回答:因为我自己使用 useReducer + useContext 自己可以轻松实现,干嘛还要用Redux。 再见 Redux。