useReducer高级用法

3/13/2023 React

React Hook useReducer高级用法

所谓高级用法,只不过是一些深层知识点和实用技巧,其实也就是对前面知识点的一个巩固和学习。

# 使用useReducer来管理复杂类型的数据

举例:若某组件内通过ajax请求数据,获取最新一条短信文字,需要组件显示整个ajax过程及结果:

  1. 当ajax开始请求时,界面显示"loading...";
  2. 当ajax请求发生错误时,界面显示"wrong";
  3. 当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('good news!');//请注意,这是一行伪代码,只是为了演示,并不是真正ajax获取的结果
        setError(false);

        //ajax请求错误
        setLoading(false);
        setError(true);
    }

    return <div>
        {loading ? 'loading...' : result}
        {error ? 'wrong!' : null}
    </div>
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

如果使用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: 'sucess', res: 'good news!'})
        
        // ajax请求错误
        dispatch({type: 'error'})
    }


    return <div>
        {state.loading ? 'loading...' : state.result}
        {state.error ? 'wrong!' : null}
    </div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  1. 为什么看上去使用了useReducer后代码变得更多?
    因为使用useReducer,将修改数据拆为2个部分,即"抛出修改事件和事件修改处理函数"。虽然代码增多了,但是逻辑更加清晰了。
  2. 为什么不使用useState,同时把它对应的变量也做成一个obj,就像useReducer的initralData那种?
    单纯从1次ajax请求很难看出使用useState或useReducer的差异,但是想一下,如果多次且ajax返回值在结构类型上容易发生变更,那么使用useReducer这种更加利于代码阅读、功能扩展等。

# 使用useContext和useReducer实现操作全局共享数据

实现以下组件需求:

  1. 父组件中定义某变量xx;
  2. 任何层级下的子组件都可以轻松获取变量xx、并且可以"修改"变量xx;

注意这里的修改是加引号的,是因为永远无法直接赋值的方式进行修改,永远都需要调用父组件提供的方法来修改。

# 需求分析

首先这个功能是类组件无法做到的,也是React 16.8版本以前根本不可能实现的,今天可以使用Hook轻松实现类似 Redux 共享数据管理功能。

# 实现原理

  1. 用 useContext 实现"获取全局数据"
  2. 用 useReducer 实现"修改全局数据"

# 实现思路

  1. 用React.createContext() 定义一个全局数据对象;
  2. 在父组件中用 useReducer 定义全局变量xx和负责抛出修改事件的dispatch;
  3. 在父组件之前,定义负责具体修改全局变量的处理函数reducer,根据修改xx事件类型和参数,执行修改xx的值;
  4. 在父组件中用 <XxxContext.Provider value={obj}>也就是{xx, dispatch} 标签把 全局共享数据和负责抛出修改xx的dispatch 暴露给子组件;
  5. 在子组件中用 useContext 获取全局变量;
  6. 在子组件中用 xxContext.dispatch 去抛出修改xx的事件,携带修改事件类型和参数;

# 补充说明

上面一直提到了"抛出事件""事件处理函数""dispatch"都是字面上的,不是真正意义上的事件驱动。这些都这是 React 暴露给我们的函数或形参。真正的事件驱动是由 React Hook 底层为我们完成的。

以上观点只是我个人理解,不能保证100%正确。

# 伪代码演示

假设React组件需求为:

  1. 有全局数据变量count;
  2. 不同层级的子组件均可获取并修改全局变量count;

共享对象

import React from 'react';
const CountContext = React.createContext();
export default CountContext;
1
2
3

父组件

import React, { useReducer } from 'react';
import CountContext from './CountContext';
import ComponentA from './ComponentA';
import ComponentB from './ComponentB';
import ComponentC from './ComponentC';

// 定义count的默认值
const initialCount = 0;

// 修改count事件处理函数,根据修改参数进行处理
const reducer = (state, action) => {
    // 先判断事件类型,然后结合携带的param 来修改最终的count
    switch (action.type) {
        case 'add':
            return state + action.param;
        case 'sub':
            return state - action.param;
        case 'mul':
            return state * action.param;
        default:
            console.log('默认');
            return state;
    }
}

const ParentComponent = () => {
    // 定义全局变量count,以及负责抛出修改事件的dispatch
   const [count, dispatch] = useReducer(reducer, initialCount);
   
   // 注意:value={{ count, dispatch }} 是整个代码的核心,把count、dispatch 暴露给所有的子组件
   return <CountContext.Provider>
   <div>
      ParentComponent - count={count}
      <ComponentA />
      <ComponentB />
      <ComponentC />
   </div>
  </CountContext.Provider>
   
}

export default ParentComponent;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

子组件A

import React, { useState, useContext } from 'react';
import CountContext from './CountContext';

const ComponentA = () => {
    
   const [param, setParam] = useState(1);
    // 引入全局共享对象,获取全局变量count,以及修改count对应的dispatch
   const countContext = useContext(CountContext);
   
   const handleChangeInput = (event) => {
       setParam(event.target.value);
   }
   
   const doHandle = () => {
       // 若想修改全局count,先获取count对应的修改抛出事件对象dispatch,然后通过dispatch将修改内容抛出去
       // 抛出的修改内容为:{type: 'add', param: xxx},告诉count的修改事件处理函数,本次修改的类型为add,参数是param
       // 这里的add和param完全是根据自己实际需求自己定义的
      countContext({type: 'add', param: Number(param) });
   }
   
   const resetHanlde = () => {
       countContext({type: 'reset'});
   }

   return <div>
      ComponentA - count={countContext.count}
      <input type='number' value={param} onChange={handleChangeInput} />
      <button onClick={doHandle}>add {param}</button>
      <button onClick={resetHanlde}>reset</button>
   </div>
}

export default ComponentA;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 总结

  1. 3个子组件他们主要区别是组件内 doHandle 函数,对count进行不同形式的修改;
  2. 3个子组件分别可以实现对全局变量 count 的获取和修改;
  3. 当任何一个子组件对count进行了修改,都会立即反映在其他子组件中,实现子组件之间的数据共享。

至此,实现了比较简单的,类似 Redux 全局管理效果。

# 为什么不使用Redux?

因为我自己使用 useReducer + useContext 自己可以轻松实现,干嘛还要用Redux。

但是业务就说后台管理系统,我用rtk就把登录用户信息,以及菜单按钮权限、以及tabs、主题切换等都还是会放到rtk里面。

其他就会用 useReducer + useContext

# 什么时候用useState?什么时候用useReducer?

个人建议是:组件自己内部的简单逻辑变量用useState、多个组件之间共享的复杂逻辑变量用useReducer。

至此,关于useReducer高级用法就到这里了,useReducer可以让我们实现复杂逻辑的数据修改,结合useContext更能做到全局数据共享和修改。

目前从2月14号第一篇到现在3月13号了,也是断断续续更新,因为还要写公司的项目,所以一般都是下班回去或者摸鱼的时候来写这些的,到现在也就4个Hook函数useState、useEffect、useContext、useReducer,他们都是用来实现组件某些具体业务功能的,接下来要干的Hook函数是用来提高组件整体性能的,例如useCallback,useMemo。

我们useCallback基础用法见。拜拜👋