useReducer是React hooks提供的API之一,它和redux的使用几乎一样。因此如果你熟悉redux,那么自然就已经知道如何去使用useReducer了。
1
我用最简单的递增递减的案例,来和大家分享一下它的用法。
最终实现效果如图。
首先从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使用案例就完成了。
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。
试着使用一下这个方法。扩展刚才的案例,实现效果如图所示。
首先定义两个初始状态,并且定义好每个状态对应的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的实现中,也调用了同样的方法。
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变得更具有竞争力。