React|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; // or import 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 located const [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 function function reducer(state, action) { switch (action.type) { // ... case 'reset': return { count: action.payload }; default: throw new Error(); } }// wherever our useReducer is located const [state, dispatch] = useReducer(reducer, initialCount, initFunc); // Updating the state with the dispatch functon on button click

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

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

不触发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的时候,是这样用:
setState(e.currentTarget.value)} />

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

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

我们知道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 ({isLoggedIn ? ( <> Welcome {username}! ) : (
{error && {error}
}Please Login!
setUsername(e.currentTarget.value)} /> setPassword(e.currentTarget.value)} />
)}); }

对于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 ({isLoggedIn ? ( <> Welcome {username}! ) : (
{error && {error}
}Please Login!
dispatch({ type: 'field', fieldName: 'username', payload: e.currentTarget.value, }) } /> dispatch({ type: 'field', fieldName: 'password', payload: e.currentTarget.value, }) } />
)}); }

在使用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 ({count} ); };

当用户点击+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 ( <>{state.count1} {state.count2} ); };

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 ( <>First Name: Last Name: ); };

当我们在TextInput 组件中自定义onChangeText 方法,这个时候通过 changeFirstName 函数,改变changeFirstName值,进而改变value值。
useReducer 结合 useContext 使用
在日常的开发中,组件之间共享state的时候,很多人使用全局的state,虽然这样可以满足需求,但是降低了组件的灵活性和扩展性,所以更优雅的一种方式是使用useContext,对于useContext不熟悉的同学可以参考react官方文档关于这一部分的讲解。在本例子中,笔者将使用useContext 和 useReducer 函数一起使用,看下面的代码:
const CountContext = React.createContext(); const CountProvider = ({ children }) => { const contextValue = https://www.it610.com/article/useReducer(reducer, initialState); return ( {children} ); }; const useCount = () => { const contextValue = https://www.it610.com/article/useContext(CountContext); return contextValue; };

useCount 函数是自定义的hook,和正常的hook使用的方式是一致的。那么组件在使用useCount 钩子的时候,可以像下面这样用:
const Counter = () => { const [count, dispatch] = useCount(); return ({count} ); }; // now use it const Example05 = () => ( <> );

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 }) => ({count} ); const Counter1 = () => { const [count, dispatch] = useCount1(); return }; const Counter2 = () => { const [count, dispatch] = useCount2(); return };

最后我们用一个函数组件封装Counter:
const Example06 = () => ( <> );

这里的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 分钟搞定。你也可以快速搭建一套属于你的后台管理工具。
React|React useReducer 终极使用教程
文章图片

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

    推荐阅读