写点什么

React useReducer 终极使用教程

作者:蒋川
  • 2022 年 8 月 31 日
    广东
  • 本文字数:11476 字

    阅读完需:约 38 分钟

React useReducer 终极使用教程

本文完整版:《React useReducer 终极使用教程


useReducer 是在 react V 16.8 推出的钩子函数,从用法层面来说是可以代替 useState。相信前期使用过 React 的前端同学,大都会经历从 class 语法向 hooks 用法的转变,react 的 hooks 编程给我们带来了丝滑的函数式编程体验,同时很多前端著名的文章也讲述了 hooks 带来的前端心智的转变,这里就不再着重强调,本文则是聚焦于 useReducer 这个钩子函数的原理和用法,笔者带领大家再一次深入认识 useReducer。


众所周知,useState 常用在单个组件中进行状态管理,但是遇到状态全局管理的时候,useState 显然不能满足我们的需求,这个时候大多数的做法是利用第三方的状态管理工具,像 redux,Recoil 或者 Mobx,在代码里就会有


import XXX from Mobx;import XXX from Redux;// orimport XXX from Recoil;
复制代码


这些三方的 import 语句。强大的 React 团队难道就不能自己实现一个全局的状态管理的 hook 吗,这不,useReducer 为了解决这个需求应运而生。 虽然有了 useReducer,但是黄金法则依旧成立:组件的状态交给组件管理,redux 负责工程的状态管理。本文则负责讲解 useReducer 是如何执行全局的状态管理,并且什么时候用合适,什么时候不合适,这里也会提及。


另外如果你正在搭建后台管理系统,又不想处理前端问题,推荐使用卡拉云,卡拉云是新一代低代码开发工具,可一键接入常见数据库及 API ,无需懂前端,仅需拖拽即可快速搭建属于你自己的后台管理工具,一周工作量缩减至一天,详见本文文末。

useReducer 工作原理

在学习一个新特性的时候,最好的方式之一是首先熟悉该特性的原理,进而可以促进我们的学习。 useReducer 钩子用来存储和更新状态,有点类似 useState 钩子。在用法上,它接收一个 reducer 函数作为第一个参数,第二个参数是初始化的 state。useReducer 最终返回一个存储有当前状态值的数组和一个 dispatch 函数,该 dispatch 函数执行触发 action,带来状态的变化。这其实有点像 redux,不过还是有一些不同,后面笔者会列举这两个概念和不同。

关于 reducer 函数

通常的,reduce 方法在数组的每一个元素上都执行 reducer 函数,并返回一个新的 value,reduce 方法接收一个 reducer 函数,reducer 函数本身会接收 4 个参数。下面这段代码片段揭示一个 reducer 是如何运行的:


const reducer = (accumulator, currentValue) => accumulator + currentValue;[2, 4, 6, 8].reduce(reducer)// expected output: 20
复制代码


在 React 中,useReducer 接收一个返回单组值的 reducer 函数,就像下面这样:


const [count, dispatch] = useReducer(reducer, initialState);
复制代码


前面提到过,这里的 reducer 函数本身会接受两个参数,第一个是 state,第二个是 action,这个 action 会被 dispatch 执行,就像是:


function reducer(state, action) { }
dispatch({ type: 'increment' })
复制代码


根据不同的 action ,reducer 函数会带来不同的 state 的变化,就像是 type 是 increment 的情况,reducer 函数会使得 state 加 1。

懒惰创建初始 state

在编程概念中,懒初始化是延迟创建对象的一种手段,类似于直到被需要的第一时间才去创建,还有其他的动作比如值的计算或者高昂的计算开销。正如上面提到的,useReducer 的第三个参数是一个可选值,可选的懒创建 state 的函数,下面的这段代码是更新 state 的函数:


const initFunc = (initialCount) => {    if (initialCount !== 0) {        initialCount=+0    }  return {count: initialCount};}
// wherever our useReducer is locatedconst [state, dispatch] = useReducer(reducer, initialCount, initFunc);
复制代码


当 initialCount 变量不为 0 的时候,赋值为 0;并返回 count 的赋值对象。注意第三个参数是一个函数,并不是一个对象或者数组,函数中可以返回对象。

dispatch 函数

dispatch 函数是触发不同 action 的函数,通常的它是接受含有 type 的一个对象,并根据这个 type 来执行对应的 action,action 执行完成之后,render 函数继续发挥作用,这时候会更新 state。当我们关注的焦点不在 useReducer 用法细节上时,我们会在宏观上看到 render 和 state 的变化过程。 组件触发的 action 都是接收含有 type 和 payload 的对象,其中 type 代表不同 action 的区别,payload 是 action 将要添加到 state 的数据。 在使用上,dispatch 用起来非常的简单,就拿 JSX 语法来讲,可以直接在组件事件上触发 action 操作,代码如下:


// creating our reducer functionfunction reducer(state, action) {  switch (action.type) {   // ...      case 'reset':          return { count: action.payload };    default:      throw new Error();  }}
// wherever our useReducer is locatedconst [state, dispatch] = useReducer(reducer, initialCount, initFunc);
// Updating the state with the dispatch functon on button click<button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button>
复制代码


注意到,reducer 函数接收 payload 作为传参,其中这个 payload 是来自 dispatch 的贡献,初始化的 state 也是会影响 payload 的。组件之间,使用 props 传递数据的时候,其实 dispatch 也是直接可以封装在函数中,这样方便的从父组件将 dispatch 传递到子组件,就像下面这样:


<Increment count={state.count} handleIncrement={() => dispatch({type: 'increment'})}/>
复制代码


在子组件中,接收 props,触发的时候,则有:


<button onClick={handleIncrement}>Increment</button>
复制代码

不触发 dispatch

如果 useReducer 返回的值和当前的一样,React 不会更新组件,也不会引起 effect 的变化,因为 React 内部使用了 Object.is 的语法。

useState 和 useReducer 比较和区别及应用场景

相信阅读 React 官方文档学习的同学,第一个接触的 Hook 就是 useState,useState 是一个基础的管理 state 变化的钩子,对于更复杂的 state 管理,甚至全局的 state 管理,useReducer 是用来干这件事情的。然而,useState 其实是使用到 useReducer 的,这意味着,只要是使用 useState 实现的,都可以使用 useReducer 去实现。 但是呢,这两个钩子 useReducer 和 useState 还是有不同的,在用 useReducer 的时候,可以避免通过组件的不同级别传递回调。useReducer 提供 dispatch 在各个组件之间进行传递,这种方式提高了组件的性能。 然而,这并不意味着每一次的渲染都会触发 useState 函数,当在项目中有复杂的 state 的时候,这时候就不能用单独的 setter 函数进行状态的更新,相反的你需要写一个复杂的函数来完成这种状态的更新。因此推荐使用 useReducer,它返回一个在重新渲染之间不会改变的 dispatch 方法,并且您可以在 reducer 中有操作逻辑。还值得注意的是,useState 最后是触发的 update 来更新状态,useReducer 则是用 dispatch 来更新状态。 接下来我们来看这两种钩子函数:useState 和 useReducer 是如何声明和使用的。

用 useState 声明 state

useState 的声明语句非常的简单,例如:


const [state, setState] = useState('default state');
复制代码


useState 返回一个保存当前 state 和更新 state 的数组,这里的 setState 是更新 state 的函数。

用 useReducer 声明 state

使用 useReducer 的时候看下面的语句:


const [state, dispatch] = useReducer(reducer, initialState)
复制代码


useReducer 返回一个保存当前 state 和一个更新 state 的 dispatch 函数。这个 dispatch 函数有点类似 setState,我们在用 setState 更新 state 的时候,是这样用:


<input type='text' value={state} onChange={(e) => setState(e.currentTarget.value)} />
复制代码


在 onChange 事件中调用 setState 更新当前的 state。对比使用 useReducer 钩子,可以这样表达:


<button onClick={() => dispatch({ type: 'decrement'})}>Decrement</button>
复制代码


这里的语意是当用户点击按钮的时候,会触发 dispatch,执行 type 是 decrement 的 action。另外在使用 dispatch 函数我们还可以传 payload:


<button onClick={() => dispatch({ type: 'decrement',payload:0})}>Decrement</button>
复制代码


我们知道 useReducer 可以处理复杂多层 state 的情况,这里笔者继续举该类情况的例子:


const [state, dispatch] = useReducer(loginReducer,  {    users: [      { username: 'Philip', isOnline: false},      { username: 'Mark', isOnline: false },      { username: 'Tope', isOnline: true},      { username: 'Anita', isOnline: false },    ],    loading: false,    error: false,  },);
复制代码


useReducer 接收一个初始对象,对象的 key 包含 users,loading,error。使用 useReducer 管理本地 state 的方便之处是用 useReducer 可以改变部分的 state,也就是说,这里可以单独改变 users。


调试 Vue UI 组件太麻烦?

试试卡拉云,无需懂前端,拖拽即可生成前端组件,连接 API 和数据库直接生成后台系统,两个月的工期降低至 1 天

useReducer 用法之可以使用的场景

在开发项目的时候,随着我们工程的体积不断的变大,其中的状态管理会越来越复杂,此时我们最好使用 useReducer。useReducer 提供了比 useState 更可预测的状态管理。当状态管理变的复杂的时候,这时候 useReducer 有着比 useState 更好的使用体验。 这里的不得不重提一个法则:当你的 state 是基础类型,像 number,boolean,string 等,这时候使用 useState 是一种更简单、更合适的选择。 下面笔者将创建一个登陆的组件,让读者体会使用 useReducer 的好处。

创建一个登陆组件

为了让我们更好的理解 useReducer 的用法,这里创建一个登陆组件,并比较一下使用 useState 和 useReducer 在状态管理用法上的异同。 首先我们先用 useState 创建登陆组件:


import React, { useState } from 'react';
export default function LoginUseState() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [isLoading, showLoader] = useState(false); const [error, setError] = useState(''); const [isLoggedIn, setIsLoggedIn] = useState(false); const onSubmit = async (e) => { e.preventDefault(); setError(''); showLoader(true); try { await function login({ username, password }) { return new Promise((resolve, reject) => { setTimeout(() => { if (username === 'ejiro' && password === 'password') { resolve(); } else { reject(); } }, 1000); }); } setIsLoggedIn(true); } catch (error) { setError('Incorrect username or password!'); showLoader(false); setUsername(''); setPassword(''); } }; return ( <div className='App'> <div className='login-container'> {isLoggedIn ? ( <> <h1>Welcome {username}!</h1> <button onClick={() => setIsLoggedIn(false)}>Log Out</button> </> ) : ( <form className='form' onSubmit={onSubmit}> {error && <p className='error'>{error}</p>} <p>Please Login!</p> <input type='text' placeholder='username' value={username} onChange={(e) => setUsername(e.currentTarget.value)} /> <input type='password' placeholder='password' autoComplete='new-password' value={password} onChange={(e) => setPassword(e.currentTarget.value)} /> <button className='submit' type='submit' disabled={isLoading}> {isLoading ? 'Logging in...' : 'Log In'} </button> </form> )} </div> </div> );}
复制代码


对于 username,password,isLoading 等的管理,都是使用的 useState 进行的处理,所以这里我们使用了五个 useState 钩子函数,面对更多的 state 的时候,有时候我们会担心我们是否可以更好的管理这些 state 呢。这时候可以尝试用 useReducer,直接在 reducer 函数中管理全部的状态。


import React, { useReducer } from 'react';
function loginReducer(state, action) { switch (action.type) { case 'field': { return { ...state, [action.fieldName]: action.payload, }; } case 'login': { return { ...state, error: '', isLoading: true, }; } case 'success': { return { ...state, isLoggedIn: true, isLoading: false, }; } case 'error': { return { ...state, error: 'Incorrect username or password!', isLoggedIn: false, isLoading: false, username: '', password: '', }; } case 'logOut': { return { ...state, isLoggedIn: false, }; } default: return state; }}const initialState = { username: '', password: '', isLoading: false, error: '', isLoggedIn: false,};export default function LoginUseReducer() { const [state, dispatch] = useReducer(loginReducer, initialState); const { username, password, isLoading, error, isLoggedIn } = state; const onSubmit = async (e) => { e.preventDefault(); dispatch({ type: 'login' }); try { await function login({ username, password }) { return new Promise((resolve, reject) => { setTimeout(() => { if (username === 'ejiro' && password === 'password') { resolve(); } else { reject(); } }, 1000); }); } dispatch({ type: 'success' }); } catch (error) { dispatch({ type: 'error' }); } }; return ( <div className='App'> <div className='login-container'> {isLoggedIn ? ( <> <h1>Welcome {username}!</h1> <button onClick={() => dispatch({ type: 'logOut' })}> Log Out </button> </> ) : ( <form className='form' onSubmit={onSubmit}> {error && <p className='error'>{error}</p>} <p>Please Login!</p> <input type='text' placeholder='username' value={username} onChange={(e) => dispatch({ type: 'field', fieldName: 'username', payload: e.currentTarget.value, }) } /> <input type='password' placeholder='password' autoComplete='new-password' value={password} onChange={(e) => dispatch({ type: 'field', fieldName: 'password', payload: e.currentTarget.value, }) } /> <button className='submit' type='submit' disabled={isLoading}> {isLoading ? 'Logging in...' : 'Log In'} </button> </form> )} </div> </div> );}
复制代码


在使用 useReducer 代替 useState 的过程中,我们会发现 useReducer 会使我们更聚焦于 type 和 action,举个例子说,当执行 login 动作的时候,会将 isLoading,error 和 state 进行赋值:


case 'login': {      return {        ...state,        error: '',        isLoading: true,      };    }
复制代码


体验好的一点是,我们再也不需要主动去更新 state,useReducer 的赋值会直接帮助我们解决所有的问题。

何时该使用 useReducer 实战应用案例

useReducer 最小化的范式

且看下面最简单的例子:


const initialState = 0;const reducer = (state, action) => {  switch (action) {    case 'increment': return state + 1;    case 'decrement': return state - 1;    case 'reset': return 0;    default: throw new Error('Unexpected action');  }};
复制代码


代码很简单,首先定义一个初始化的 state:initialState = 0;之后在 reducer 函数中通过 switch 来对 state 执行不同的操作。注意到,这里的 state 其实是个 number 对象,这在 Redux 的使用者看来或许有一些疑惑,因为在 redux 中都是用 object 来处理的。这其实是 useReducer 的方便之处。 在组件中,常常会有点击事件带来状态变化的情况,比如说购物车组件中商品数量的增加,点击加号商品数量会加一,这个时候上面的代码就可以应用到组件中,例如:


const Example01 = () => {  const [count, dispatch] = useReducer(reducer, initialState);  return (    <div>      {count}      <button onClick={() => dispatch('increment')}>+1</button>      <button onClick={() => dispatch('decrement')}>-1</button>      <button onClick={() => dispatch('reset')}>reset</button>    </div>  );};
复制代码


当用户点击+1 的按钮时,dispatch 会出发 increment 的 action,count +1 ,所以会看到 state 变化后的结果。这种 type 其实可以定义很多,选择合适的数量即可。

useReducer action 对象

下面的例子其实有点像 redux 的用法,习惯 redux 的同学可能会比较熟悉:


const initialState = {  count1: 0,  count2: 0,};const reducer = (state, action) => {  switch (action.type) {    case 'increment1':      return { ...state, count1: state.count1 + 1 };    case 'decrement1':      return { ...state, count1: state.count1 - 1 };    case 'set1':      return { ...state, count1: action.count };    case 'increment2':      return { ...state, count2: state.count2 + 1 };    case 'decrement2':      return { ...state, count2: state.count2 - 1 };    case 'set2':      return { ...state, count2: action.count };    default:      throw new Error('Unexpected action');  }};
复制代码


初始化的 state 是一个对象,并且 return 出去的也是一个对象。和前面的那个例子相比,除了多了不同的 case 之外,在更新 state 通过对象赋值的方式进行。initialState 对象中是有两个 key,在更新的时候针对指定的 key 更新即可。上面的例子看起来有些复杂,把它用到组件上,会简化使用过程:


const Example02 = () => {  const [state, dispatch] = useReducer(reducer, initialState);  return (    <>      <div>        {state.count1}        <button onClick={() => dispatch({ type: 'increment1' })}>+1</button>        <button onClick={() => dispatch({ type: 'decrement1' })}>-1</button>        <button onClick={() => dispatch({ type: 'set1', count: 0 })}>reset</button>      </div>      <div>        {state.count2}        <button onClick={() => dispatch({ type: 'increment2' })}>+1</button>        <button onClick={() => dispatch({ type: 'decrement2' })}>-1</button>        <button onClick={() => dispatch({ type: 'set2', count: 0 })}>reset</button>      </div>    </>  );};
复制代码


Example2 组件中,上半部分显示的是 count1 的变化,下半部分则是显示 count2 的变化。也是通过点击 button 来触发 dispatch,引起 state 变化。

useReducer 在文本框组件中使用

前面的两个例子都是通过 button 上面的 onClick 事件来触发,在平时的业务开发中,输入框组件的 onChange 事件也是我们常使用的方法,此时我们也可以结合 useReducer 来结合输入框的 value 属性使用,做到实时展示输入的内容,使得组件受控,见下面的代码:


const initialState = '';const reducer = (state, action) => action;
const Example03 = () => { const [firstName, changeFirstName] = useReducer(reducer, initialState); const [lastName, changeLastName] = useReducer(reducer, initialState); return ( <> <div> First Name: <TextInput value={firstName} onChangeText={changeFirstName} /> </div> <div> Last Name: <TextInput value={lastName} onChangeText={changeLastName} /> </div> </> );};
复制代码


当我们在 TextInput 组件中自定义 onChangeText 方法,这个时候通过 changeFirstName 函数,改变 changeFirstName 值,进而改变 value 值。

useReducer 结合 useContext 使用

在日常的开发中,组件之间共享 state 的时候,很多人使用全局的 state,虽然这样可以满足需求,但是降低了组件的灵活性和扩展性,所以更优雅的一种方式是使用 useContext,对于 useContext 不熟悉的同学可以参考 react 官方文档关于这一部分的讲解。在本例子中,笔者将使用 useContext 和 useReducer 函数一起使用,看下面的代码:


const CountContext = React.createContext();
const CountProvider = ({ children }) => { const contextValue = useReducer(reducer, initialState); return ( <CountContext.Provider value={contextValue}> {children} </CountContext.Provider> );};
const useCount = () => { const contextValue = useContext(CountContext); return contextValue;};
复制代码


useCount 函数是自定义的 hook,和正常的 hook 使用的方式是一致的。那么组件在使用 useCount 钩子的时候,可以像下面这样用:


const Counter = () => {  const [count, dispatch] = useCount();  return (    <div>      {count}      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>      <button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>    </div>  );};
// now use itconst Example05 = () => ( <> <CountProvider> <Counter /> <Counter /> </CountProvider> <CountProvider> <Counter /> <Counter /> </CountProvider> </>);
复制代码


useCount 会走内部的 useReducer,这个时候通过 dispatch 函数会改变对应的 state 的状态。

useReducer 订阅的需要

Context 使用的场景其实是在组件之间,但是如果在组件的外部,这个时候我们需要使用订阅来做。这个时候我们可以订阅一个共享的 state,并当 state 更新的时候去更新组件。对于前面的那个使用 Context 的例子,这里我们用订阅实现一下。 第一步,首先写一个最简单的 useReducer:


const useForceUpdate = () => useReducer(state => !state, false)[1];
复制代码


接下里写一个函数创建共享的 state 并返回一个钩子函数:


const createSharedState = (reducer, initialState) => {  const subscribers = [];  let state = initialState;  const dispatch = (action) => {    state = reducer(state, action);    subscribers.forEach(callback => callback());  };  const useSharedState = () => {    const forceUpdate = useForceUpdate();    useEffect(() => {      const callback = () => forceUpdate();      subscribers.push(callback);      callback(); // in case it's already updated      const cleanup = () => {        const index = subscribers.indexOf(callback);        subscribers.splice(index, 1);      };      return cleanup;    }, []);    return [state, dispatch];  };  return useSharedState;};
复制代码


这里我们使用了 useEffect 钩子函数,在这个钩子函数中,我们订阅一个回调函数来更新组件,当组件卸载的时候,我们也会清除订阅。 接下来我们创建两个共享的 state:


const useCount1 = createSharedState(reducer, initialState);const useCount2 = createSharedState(reducer, initialState);
复制代码


用一下这个钩子函数:


const Counter = ({ count, dispatch }) => (  <div>    {count}    <button onClick={() => dispatch({ type: 'increment' })}>+1</button>    <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>    <button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>  </div>);
const Counter1 = () => { const [count, dispatch] = useCount1(); return <Counter count={count} dispatch={dispatch} />};
const Counter2 = () => { const [count, dispatch] = useCount2(); return <Counter count={count} dispatch={dispatch} />};
复制代码


最后我们用一个函数组件封装 Counter:


const Example06 = () => (  <>    <Counter1 />    <Counter1 />    <Counter2 />    <Counter2 />  </>);
复制代码


这里的 count 的更新都是使用共享的 useCount 钩子函数。

useReducer 用法之不该使用的场景

这是一个好的问题,前面介绍了使用 useReducer 的情况,但是什么时候我们不可以用 useReducer 呢。 为了更好的理解这个问题,笔者首先说一下使用 useReducer 基本的心智,useReducer 是可以帮助我们管理复杂的 state , 但是我们也不应该忽略 redux 在某些情况下可能是更好的选择。 最开始我们的想法是我们尽量避免使用第三方的 state 管理工具,当你有疑惑是否要使用他们时,说明这不是用他们的时候。 下面笔者列举几个使用 Redux 和 Mobx 的例子。

当你的应用需要单一的来源时

当前端的应用通过接口获取数据,且这个数据源就是从这个接口获取的,这个时候使用 Redux 可以更方便的管理我们的 state,就像是写一个 todo/undo demo,直接可以使用 Redux。

当你需要一个更可预测的状态

当你的应用运行在不同的环境中时,使用 Redux 可以使得 state 的管理变得更稳定。同样的 state 和 action 传到 reducer 的时候,会返回相同的结果。并且 redux 不会带来副作用,只有 action 会使其更改状态。

当状态提升到顶部组件

当需要在顶部组件处理所有的状态的时候,这时候使用 Redux 是更好的选择。

React useReducer 教程总结

到这里 useReducer 的使用场景和用法例子讲解都已经介绍完成了,最后我们回顾一下,首先类比于 redux 的 reducer,useReducer 的思路和 redux 一样,不同点是在于 useReducer 最终操作的对象是 state。在使用上,就拿最简单的 button 组件为例子,点击的时候触发 dispatch,根据 type 修改 state。复杂一点的,可以结合 useContext 使用,满足多个组件共享 state 的情况。 总之,在掌握用法之后,多在项目中实践,learn by doing ,是较为有效的掌握知识的方式。


其实如果你根本不想处理复杂的 React 前端问题,完全可以使用卡拉云来搭建前端工具,卡拉云内置多种常用组件,无需懂任何前端,仅需拖拽即可快速生成。


下面是用卡拉云搭建的数据库 CURD 后台管理系统,只需拖拽组件,即可在 10 分钟内完成搭建。


可直接分享给同事一起使用: https://my.kalacloud.com/apps/8z9z3yf9fy/published


卡拉云可帮你快速搭建企业内部工具,下图为使用卡拉云搭建的内部广告投放监测系统,无需懂前端,仅需拖拽组件,10 分钟搞定。你也可以快速搭建一套属于你的后台管理工具。



卡拉云是新一代低代码开发平台,与前端框架 Vue、React 等相比,卡拉云的优势在于不用首先搭建开发环境,直接注册即可开始使用。开发者完全不用处理任何前端问题,只需简单拖拽,即可快速生成所需组件,可一键接入常见数据库及 API,根据引导简单几步打通前后端,数周的开发时间,缩短至 1 小时。立即免费试用卡拉云


扩展阅读:


发布于: 2022 年 08 月 31 日阅读数: 52
用户头像

蒋川

关注

我的微信:HiJiangChuan 2020.09.08 加入

卡拉云 CMO 卡拉云是一套帮助后端程序员搭建企业内部工具的系统,欢迎试用 www.kalacloud.com

评论

发布
暂无评论
React useReducer 终极使用教程_JavaScript_蒋川_InfoQ写作社区