Kothing
Author of Kothing, a Bootstrap Medium styled template available for WordPress, HTML, Ghost and Jekyll. You are currently previewing Jekyll template demo.

React Hook - useReducer示例详解

useReducer 它接收一个形如 (state, action) => newState 的 reducer方法,并返回当前的 state 以及与其配套的 dispatch 方法。useState 的替代方案。 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

reducer概念

语法:

1
const [state, dispatch] = useReducer(reducer, initialArg, init);

什么是reducer

reducer的概念是伴随着Redux的出现逐渐在JavaScript中流行起来。但我们并不需要学习Redux去了解Reducer。简单来说 reducer是一个函数(state, action) => newState:接收当前应用的state和触发的动作action,计算并返回最新的state。以下是用 reducer 重写 useState 一节的计数器示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const initialState = {count: 0};

//根据state(当前状态)和action(触发的动作加、减)参数,计算返回newState
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>
    </>
  );
}

上面例子:state是一个number类型的数值,reducer根据action的类型(加、减)对应的修改state,并返回最终的state。为了刚接触到reducer概念的小伙伴更容易理解,可以将state改为myState,但请始终牢记count仍然是state。

1
2
3
4
5
6
7
8
9
10
11
function reducer(myState, action) {
  switch (action.type) {
    case 'increment':
      return {count: myState.count + 1};
    case 'decrement':
      return {count: myState.count - 1};
    default:
      throw new Error();
  }
}
...

reducer 的幂等性

从上面的示例可以看到reducer本质是一个纯函数,没有任何UI和副作用。这意味着相同的输入(state、action),reducer函数无论执行多少遍始终会返回相同的输出(newState)。因此通过reducer函数很容易推测state的变化,并且也更加容易单元测试。

1
2
3
expect(countReducer(1, { type: 'add' })).equal(2); // 成功
expect(countReducer(1, { type: 'add' })).equal(2); // 成功
expect(countReducer(1, { type: 'sub' })).equal(0); // 成功

state 和 newState

state是当前应用状态对象,可以理解就是我们熟知的React里面的state。

在上面的例子中state是一个基础数据类型,但很多时候state可能会是一个复杂的JavaScript对象,如上例中count有可能只是 state中的一个属性。针对这种场景我们可以使用ES6的结构赋值:

1
2
3
4
5
6
7
8
9
10
11
// 返回一个 newState (newObject)
function countReducer(state, action) {
    switch(action.type) {
        case 'add':
            return { ...state, count: state.count + 1; }
        case 'sub':
            return { ...state, count: state.count - 1; }
        default: 
            return count;
    }
}

注意:

  • reducer处理的state对象必须是immutable,这意味着永远不要直接修改参数中的state对象,reducer函数应该每次都返回一个新的state object。
  • 既然reducer要求每次都返回一个新的对象,我们可以使用ES6中的解构赋值方式去创建一个新对象,并复写我们需要改变的state属性,如上例。

如果state是多层嵌套,解构赋值实现就非常复杂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function bookReducer(state, action) {
    switch(action.type) {
        // 添加一本书
        case 'addBook':
            return {
                ...state,
                books: {
                    ...state.books,
                    [bookId]: book,
                }
            };
        case 'sub':
            // ....
        default: 
            return state;
    }
}

对于这种复杂state的场景推荐使用immer等immutable库解决。

state为什么需要immutable?

  • reducer的幂等性 我们上文提到过reducer需要保持幂等性,更加可预测、可测试。如果每次返回同一个state,就无法保证无论执行多少次都是相同的结果

  • React中的state比较方案 React在比较oldState和newState的时候是使用Object.is函数,如果是同一个对象则不会出发组件的rerender。 可以参考官方文档bailing-out-of-a-dispatch。

action 理解

action:用来表示触发的行为。

用type来表示具体的行为类型(登录、登出、添加用户、删除用户等) 用payload携带的数据(如增加书籍,可以携带具体的book信息),我们用上面addBook的action为例:

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
const action = {
    type: 'addBook',
    payload: {
        book: {
            bookId,
            bookName,
            author,
        }
    }
}
function bookReducer(state, action) {
    switch(action.type) {
        // 添加一本书
        case 'addBook':
            const { book } = action.payload;
            return {
                ...state,
                books: {
                    ...state.books,
                    [book.bookId]: book,
                }
            };
        case 'sub':
            // ....
        default: 
            return state;
    }
}

总结: reducer是一个利用action提供的信息,将state从A转换到B的一个纯函数,具有一下几个特点:
语法:(state, action) => newState
Immutable:每次都返回一个newState, 永远不要直接修改state对象
Action:一个常规的Action对象通常有type和payload(可选)组成
type: 本次操作的类型,也是 reducer 条件判断的依据
payload: 提供操作附带的数据信息

reducer示例

这里使用Login登陆为示例

seState实现方式:

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
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
    )
}

上面Demo我们定义了5个state来描述页面的状态,在login函数中当登录成功、失败时进行了一系列复杂的state设置。可以想象随着需求越来越复杂更多的state加入到页面,更多的setState分散在各处,很容易设置错误或者遗漏。

useReducer实现方式:

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
43
44
45
46
47
48
49
50
51
52
53
54
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
    )
}

乍一看useReducer改造后的代码反而更长了,但很明显第二版有更好的可读性,我们也能更清晰的了解state的变化逻辑。

可以看到login函数现在更清晰的表达了用户的意图,开始登录login、登录success、登录error。LoginPage不需要关心如何处理这几种行为,那是loginReducer需要关心的,表现和业务分离。

另一个好处是所有的state处理都集中到了一起,使得我们对state的变化更有掌控力,同时也更容易复用state逻辑变化代码,比如在其他函数中也需要触发登录error状态,只需要dispatch({ type: 'error' })

useReducer可以让我们将what和how分开。比如点击了登录按钮,我们要做的就是发起登陆操作dispatch({ type: 'login' }),点击退出按钮就发起退出操作dispatch({ type: 'logout' }),所有和how相关的代码都在reducer中维护,组件中只需要思考What,让我们的代码可以像用户的行为一样,更加清晰。

除此之外还有一个好处,我们在前文提过Reducer其实一个UI无关的纯函数,useReducer的方案是的我们更容易构建自动化测试用例。