超性感的React Hooks(七)useReducer

简介: Reducer函数接收两个参数,第一个参数是当前的最新状态值,第二参数则用于告诉Reducer当前执行了什么操作。Reducer会根据不同的操作执行不同的逻辑去修改state。因此我们称第二个参数为Action。在这个简单的案例中,Action被我们定义成为一个字符串,reducer内部会根据不同的字符串,执行不同的修改状态逻辑。

useReducer是React hooks提供的API之一,它和redux的使用几乎一样。因此如果你熟悉redux,那么自然就已经知道如何去使用useReducer了。


1


我用最简单的递增递减的案例,来和大家分享一下它的用法。


最终实现效果如图。


微信图片_20220510131945.gif


首先从React中引入


import React, { useReducer } from ‘react’;


然后需要定义一个状态值,以表示useReducer维护的数据格式。此时的状态比较简单,只需要指定为一个数字即可。初始化设置为0


在redux中,我们称这样的状态值为Store


const initialState: number = 0;


然后我们需要定义一个Reducer,Reducer是一个函数。在该函数中,我们需要指定状态的改变方式。


const reduer = (state: number, action: string) => {
  switch(action) {
    case ‘increment’: return state + 1;
    case ‘decrement’: return state - 1;
    case 'reset': return 0;
    default:
      return state;
  }
}


Reducer函数接收两个参数,第一个参数是当前的最新状态值,第二参数则用于告诉Reducer当前执行了什么操作。Reducer会根据不同的操作执行不同的逻辑去修改state。

因此我们称第二个参数为Action


在这个简单的案例中,Action被我们定义成为一个字符串,reducer内部会根据不同的字符串,执行不同的修改状态逻辑。


Store, Reducer, Action是Redux的三大核心概念,同时也是useReducer的三大核心概念。


三大核心准备好之后,我们就可以定义函数组件,并在其中使用useReducer了。


export default function Counter() {
  const [counter, dispatch] = useReducer(reduer, initialState);
  return (
    <div style={{ width: ‘200px’, margin: 'auto' }}>
      <div style={{ width: '40px’, margin: ‘100px auto’, fontSize: ‘40px’ }}>{counter}</div>
      <Button onClick={() => dispatch('increment')}>递增</Button>
      <Button onClick={() => dispatch(‘decrement’)}>递减</Button>
      <Button onClick={() => dispatch('reset')}>重置</Button>
    </div>
  );
}


从useReducer的返回结果中,我们能够通过解构拿到当前Store的最新值,以及触发器dispatch。


dispatch接收Action作为参数,当dispatch调用时,会将Action告诉Reducer,Reducer通过Action修改Store。一个简单useReducer使用案例就完成了。


微信图片_20220510131950.jpg


2


在实践中,redux的使用并非都是如此简单。反而因为复杂的概念以及较高的维护成本,它的开发体验一直是一个难以解决的痛点。那么redux是如何一步一步变得复杂的呢?


难以维护的Action


上面的例子中,Action非常简单,只是一个字符串。因为我们改变状态只需要递增+1即可。那如果,我们想要增加任意的数额呢?Action就不能只是字符串了。此时Action应该变成如下所示



dispatch({ type: ‘increment’, payload: 10 })


多处使用时


dispatch({ type: ‘increment’, payload: 1 })
dispatch({ type: ‘increment’, payload: 2 })
dispatch({ type: ‘increment’, payload: 23 })
...


为了简化写法,许多人推荐的方案是创建一个Action生成函数。


function increment(state) {
  return {
    type: 'increment’,
      payload: state
  }
}


从结果上来看,使用确实简单一些了


dispatch(increment(1))
dispatch(increment(2))
dispatch(increment(23))
...


但是代码可读性急剧降低。


一个大型项目中,需要修改的状态至少数以千计,要维护这么多的Action,人都要疯。


复杂度无法预知的Store


实践中的Store可不仅仅只是一个数字。


如果将redux的Store从顶层父组件注入,这个Store的复杂度在大型项目中一定会远超想象,而且会随着项目内容的增加,还会变得越来越复杂。


const state = {
  pageHomeState: {
    list: [],
    loading: false,
    data: {
      videos: [],
      movies: [],
      author: ‘’
    },
  },
  pageOneState: {},
  pageOneState: {},
  pageOneState: {},
}


复杂到令人绝望的Reducer


当Store变得复杂,专门用于修改Store的Reducer函数也不可避免的会超级复杂。例如我们想要修改上面例子中的author,那么reducer可能会这么写


function reducer(state, action) {
  switch(action.type) {
    case ‘author’:
      return {
        …state,
        pageHomeState: {
          …state.pageHomeState,
          data: {
            ...state.pageHomeState.data,
            author: action.payload
          }
        }
      }
  }
}


这简直就是灾难。


和redux不同的是,useReducer并没有围绕这些痛点提供对应的解决方案。因此如果你想要在项目中使用useReducer,仅仅只建议小范围使用,把复杂度控制在可以接受的范围之内。


3


在Redux中,借助它提供的combineReducer方法,我们可以将多个Reducer合并为一个。这让我们在实践时,可以将整个大的Reducer进行拆分,以减少复杂度。只需要在最后调用该方法合并即可。


我们也可以自己实现这个方法。


首先,我们的目的其实是想要拆分Store,只要Store变得简单,对应的reducer也会变得更好维护。


所以需求有两个,一个是拆分Store,另一个是拆分对应的Reducer。


具体方法实现如下:


interface IterationOB {
  [key: string]: any
}
// 参数为一个对象,里面有所有的reducer
const combineReducer = (reducers: IterationOB) => {
  // 取得所有 key
  const reducerKeys = Object.keys(reducers);
  // 合并之后的State放在这里
  let objInitState: IterationOB = {};
  // 检查每一项是否有默认值
  reducerKeys.forEach((key) => {
    // 传入空的type,获取默认值,这样写了之后,action的类型就只能是 { type: 'xxx', } 这种格式了
    const initState = reducers[key](undefined, { type: '' })
    if (initState == 'undefined'){
      throw new Error(`${key} does not return state.`)
    }
    objInitState[key] = initState;
  })
  return (state: any, action: any) => {
    if(action){
      reducerKeys.forEach((key) => {
        const previousState = objInitState[key];
        objInitState[key] = reducers[key](previousState, action);
      })
    }
    return { …objInitState };
  }
}
export default combineReducer;


代码的实现思路来自于redux。


从代码中可以看出,该方法并非真正意义上合并了reduer,而是通过闭包的方式,执行所有的reducer,返回了一个合并之后的Store。


试着使用一下这个方法。扩展刚才的案例,实现效果如图所示。


微信图片_20220510131954.gif


首先定义两个初始状态,并且定义好每个状态对应的reducer函数。然后通过我们自己定义的combineReducer方法合并reducer。


import combineReducer from ‘./combineReducer’;
interface Action {
  type: string,
  payload: number
}
const stateA: number = 0
function reducerA(state = stateA, action: Action) {
  switch (action.type) {
    case ‘incrementA’: 
      return state + action.payload
    case ‘decrementA’:
      return state - action.payload
    default:
      return state;
  }
}
const stateB: number = 0
function reducerB(state = stateB, action: Action) {
  switch (action.type) {
    case 'incrementB': 
      return state + action.payload
    case 'decrementB':
      return state - action.payload
    default:
      return state;
  }
}
export default combineReducer({reducerA, reducerB});


最后定义函数组件。


import React, { useReducer } from ‘react’;
import { Button } from ‘antd-mobile’;
import reducer from ‘./reducer’;
export default function Counter() {
  const [counter, dispatch] = useReducer(reducer, reducer());
  console.log(counter);
  return (
    <div style={{ width: ‘200px’, margin: ‘auto’ }}>
      <div style={{ width: '40px', margin: '100px auto’, fontSize: ‘40px’ }}>{counter.reducerA}</div>
      <Button onClick={() => dispatch({type: 'incrementA', payload: 10})}>递增A</Button>
      <Button onClick={() => dispatch({type: 'decrementA', payload: 10})}>递减A</Button>
      <div style={{ width: '40px', margin: '100px auto', fontSize: ‘40px’ }}>{counter.reducerB}</div>
      <Button onClick={() => dispatch({type: ‘incrementB’, payload: 10})}>递增B</Button>
      <Button onClick={() => dispatch({type: ‘decrementB’, payload: 10})}>递减B</Button>
    </div>
  );
}


这个简单的例子就这样实现了。


和useState相比,使用reducer实现这样简单的案例,反而让代码更加冗余。因此在使用useReducer时,应该评估好当前应用场景。


当使用useState需要定义太多独立的state时,我们就可以考虑使用useReducer来降低复杂度。


不过当应用场景更加复杂,useReducer也许就不再适用。


分享一个小的知识点:


useState在更新时,源码中调用的方法叫做updateReducer,而在useReducer的实现中,也调用了同样的方法。



微信图片_20220510131958.jpg

微信图片_20220510132001.png


4


React hooks能取代redux吗?


有很多人写文章在鼓吹react hooks可以取代redux,大概也许是因为useReducer以及以后我们会介绍的useContext的存在。


前面我们也提到过,redux的开发思维,在实践中有非常多的痛点。redux围绕这些痛点,社区提供了非常多的优秀解决方案。但是到目前为止,useReducer并没有。因此,如果你试图替换redux,那你肯定要为此付出更多的代价。


而redux也提供了一些自定义的hooks方法,让redux的使用变得更加简单。


例如下面这个案例。仍然是经典的计数案例。


使用新的hooks之前


import React from 'react';
import {connect} from 'react-redux';
import * as actions from '../actions/actions';
class App extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    const {count, increment, decrement} = this.props;
    return (
      <div>
        <h1>The count is {count}</h1>
        <button onClick={() => increment(count)}>+</button>
        <button onClick={() => decrement(count)}>-</button>
      </div>
    );
  }
}
const mapStateToProps = store => ({
  count: store.count
});
const mapDispatchToProps = dispatch => ({
  increment: count => dispatch(actions.increment(count)),
  decrement: count => dispatch(actions.decrement(count))
});
export default connect(mapStateToProps, mapDispatchToProps)(App);


使用新的hooks之后,


import React from 'react';
import * as actions from '../actions/actions';
import {useSelector, useDispatch} from 'react-redux';
const App = () => {
  const dispatch = useDispatch();
  const count = useSelector(store => store.count);
  return (
    <div>
      <h1>The count is {count}</h1>
      <button onClick={() => dispatch(actions.increment(count))}>+</button>
      <button onClick={() => dispatch(actions.decrement(count))}>-</button>
    </div>
  );
}
export default App;


利用自定义的useSelector与useDispatch,代码复杂度降低了很多。因此确切来说,React Hooks的出现,让redux变得更具有竞争力。

相关文章
|
4天前
|
设计模式 存储 前端开发
React开发设计模式及原则概念问题之自定义Hooks的作用是什么,自定义Hooks设计时要遵循什么原则呢
React开发设计模式及原则概念问题之自定义Hooks的作用是什么,自定义Hooks设计时要遵循什么原则呢
|
1月前
|
前端开发 JavaScript 安全
TypeScript在React Hooks中的应用:提升React开发的类型安全与可维护性
【7月更文挑战第17天】TypeScript在React Hooks中的应用极大地提升了React应用的类型安全性和可维护性。通过为状态、依赖项和自定义Hooks指定明确的类型,开发者可以编写更加健壮、易于理解和维护的代码。随着React和TypeScript的不断发展,结合两者的优势将成为构建现代Web应用的标准做法。
|
6天前
|
前端开发
react中 useContext 和useReducer的使用
react中 useContext 和useReducer的使用
react中 useContext 和useReducer的使用
|
13天前
|
前端开发 开发者
彻底颠覆!React Hooks带来前端开发的革命,你准备好了吗?
【8月更文挑战第6天】在现代Web开发中,React作为顶级前端框架,以高效性能和丰富生态著称。React Hooks自16.8版本引入,赋予函数组件使用状态和生命周期的能力,使代码更简洁、模块化,易于维护。常用Hooks如`useState`、`useEffect`等简化了状态管理和副作用操作。Hooks不仅增强了组件能力,提高了代码可读性和可维护性,还在各种应用场景中展现出强大功能,尤其是在中大型项目中优化了代码结构和提升了开发效率。总之,React Hooks为前端开发注入了新活力,推动了更高效便捷的开发实践。
26 1
|
1月前
|
前端开发
React Hooks实战技巧:提升你的组件开发效率
【7月更文挑战第16天】React Hooks为React开发带来了革命性的变化,使得函数式组件更加强大和灵活。通过掌握上述实战技巧,你可以更高效地编写清晰、可维护和可复用的React组件。希望这些技巧能帮助你在React开发之路上走得更远。
|
10天前
|
JavaScript 前端开发 API
浅谈:为啥 Vue 和 React 都选择了 Hooks?
浅谈:为啥 Vue 和 React 都选择了 Hooks?
|
1月前
|
前端开发 JavaScript 开发者
前端框架与库 - React生命周期与Hooks
【7月更文挑战第13天】React 框架革新UI构建,引入Hooks简化组件状态管理和副作用处理。组件生命周期涉及挂载、更新、卸载,对应不同方法,如`componentDidMount`、`shouldComponentUpdate`等,但现在推荐使用`useState`和`useEffect` Hooks。`useEffect`处理副作用,需注意清理和依赖数组。避免问题的关键在于正确使用Hooks和理解其工作模式,以构建高效应用。
|
1月前
|
前端开发 JavaScript 数据格式
react18【系列实用教程】Hooks (useState,useReducer,useRef,useEffect,useContext,useMemo,useCallback,自定义 Hook )
react18【系列实用教程】Hooks (useState,useReducer,useRef,useEffect,useContext,useMemo,useCallback,自定义 Hook )
40 1
|
1月前
|
存储 前端开发 JavaScript
react hooks 学习进阶
【7月更文挑战第12天】 React Hooks(自16.8版起)让函数组件能处理状态和副作用。useState用于添加状态管理,useEffect处理副作用,useContext共享数据,useReducer处理复杂状态逻辑,useRef获取引用。进阶技巧涉及性能优化,如useMemo和useCallback,以及遵循规则避免在不适当位置调用Hooks。理解异步更新机制和结合Redux等库提升应用复杂性管理。持续学习新技巧是关键。
30 0
|
1月前
|
前端开发
react18【系列实用教程】Hooks 闭包陷阱 (2024最新版)含useState 闭包陷阱,useEffect 闭包陷阱,useCallback 闭包陷阱
react18【系列实用教程】Hooks 闭包陷阱 (2024最新版)含useState 闭包陷阱,useEffect 闭包陷阱,useCallback 闭包陷阱
27 0