阅读 324

【react】context VS redux

前言

自从新的context APIhook特性相继出来后,江湖上类似于“我们再也不需要redux”,“redux已死”的论调甚上尘嚣。如果不能在使用redux的过程中,保持一个深度思考状态,你可能也人云亦云,甚至为这些论调做出一些推波助澜的举动。这好像不能怪你,因为context API, context API + useReducerredux + react-redux 三者之间的功能差异如果不加以仔细审查的话,是难以区分的。所以,我们今天就来理一理这三者之间的区别是什么,各自使用的场景是什么。

正文

Context API

什么是 Context API ?

首先,需要明确的是,本文中提到的Context API都是指【新版Context API】。新版Context API 是在 react 16.3 版本所引入的。在这个版本之前的 context API我们称之为旧版 context API。它们都是为了解决同样的问题:“跨组件层级传递props的效率问题(prop-drilling)”。在没有提出 旧版 context API之前,如果我们需要跨组件层级去传递props问题,我们需要手动地层层传递。所跨层级越多,那么就越显得繁琐和低效。为了解决这个问题,react团队就提出了旧版 context API。有了这个旧版 context API,我们就只需要在父组件那里声明一下需要跨层级传递props的数据结构和类型,那么,在需要用到props的子组件再声明一次就可以通过this.context.xxx来访问到xxx这个prop。

但是,旧版 context API有一个重大的设计缺陷。那就是“传递截断”问题。所谓的“传递截断”就是指在如果在使用旧版 context API来跨层级去传递props过程中,假如承载了数据源的父组件和最终接收数据的子组件之间的某个组件通过shouldComponentUpdate来跳过自己的更新的话(shouldComponentUpdate函数里面return false),那么子组件也会被动跳过更新,因而无法拿到最新的prop值。

新版Context API就是为了弥补这个设计缺陷而实现的,同时也解决了一个语法冗余的问题。旧版 context API很快就被淘汰了,因此关于它的语法就废话不说了。下面说一说,新版 context API的语法问题。

怎么用?

使用Context API遵循下面三个步骤:

  1. 首先,调用const MyContext = React.createContext()去创建一个context 对象实例;
  2. 其次,使用<MyContext.Provider value={someValue}>组件去声明你想要传递的任何数据;
  3. 最后,通过render props范式<MyContext.Consumer>{(someValue)=> ....}</MyContext.Consumer>或者hook的写法const theContextValue = useContext(MyContext)来把想要读取的数据取回来再进行消费。

新版Context API的实现里面,只要某个子组件订阅了context对象实例所承载的数据,只要这些数据发生了改变,不管该子组件的上层组件做了什么,这个子组件都会得到更新。

Context API + useReducer

Context API 设计之处,主要是为了解决静态数据的跨组件层级传递的效率问题。当然,你也可以用它来传递动态数据 - 组件状态。这种情况下,那就变成了另外一种范式:Context API版的redux范式。

实现了什么功能?

在hook的世界里面,如果状态结构简单的话,那么我们可以使用useState来创建组件状态,如果状态结果比较复杂的话,那么我们就可以使用useReducer来创建组件状态。不过,其实在react源码内部,useState也是也只是内置了reducer的useReducer,关于这一点,可以查看我的《【react】react hook运行原理解析》。所以,我这里的“Context API版的redux范式”就是指“Context API + useReducer”。

通过将Context API的跨任何组件层级运输数据的能力和useReducer组件状态创建和更新能力结合起来,最终把它们提升到组件树的根组件,我们就可以实现“Context API版的redux范式。

怎么用?

  1. 在根组件上(假设是<App>),使用useReducer来创建状态:

    // App.js
    const initialState = {count: 0};
    
    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 App(){
        const [state,dispatch] = useReducer(reducer,initialState)
    
        return (......)
    }
    复制代码
  2. 在根组件上,使用Context API来创建context对象实例,并使用它的Provider将状态和改变状态的方法(这里是dispatch)传递下去:

    // App.js
    const initialState = {count: 0};
    export const MyContext = React.createContext()
    
    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 App(){
        const [state,dispatch] = React.useReducer(reducer,initialState)
    
        return (
            <MyContext.Provider value={{state,dispatch}}>{.....}</MyContext.Provider>
        )
    }
    复制代码
  3. 最后是在需要消费的子组件里面导入context实例对象,使用useContext()来将状态和更新状态的方法取回来:

    // ChildComponent.js
    import {MyContext} from './App'
    
    
    function ChildComponent(){
        const {state,dispatch} = useContext(MyContext)
        
        return (
            <div>
             current count is {state.count}
             <button onClick={() => dispatch({type: 'decrement'})}>-</button>
             <button onClick={() => dispatch({type: 'increment'})}>+</button>
            <div>
        )
    }
    
    复制代码

通过上面三个步骤,我们就可以实现一个redux范式。在这个实现里面:Context API负责数据的分发,useReducer复杂状态的管理(创建状态,更新状态),最后通过在根组件上把两者结合起来,那么子组件就可以通过useContext这个hook来消费了。

redux + react-redux

什么是 redux ?

当我们提起 “redux” 的时候,一般它有广义和狭义两种含义:狭义上它是单纯是指redux.js这个类库,也就是所谓的redux core;而广义上,它是指以redux.js为核心的Flux框架(围绕其中的有react,react-redux, redux-toolkit等类库),这个框架的核心概念有“store”,“state”,“view”,“dispatch”,“action”,“reducer”等。在这个小节,这里的“redux”是包含了上面广义和狭义的两种含义。

看看官方文档怎么说:

Redux is a pattern and library for managing and updating application state, using events called "actions". It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.

The patterns and tools provided by Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur.

注意,上面的阐述中强调了两点:

  • redux是一个【状态管理】的框架;
  • 这个框架能够很好地帮助你明白状态在“什么时候”,“在哪里”,“为什么”,“发生了怎样的”改变。

自从redux core 团队推出 redux-toolkit之后,编写 redux 应用效率和体验也越来越好了,在这里,我就不赘述【怎样用 redux】了,感兴趣的自己去官网看看。

什么是 react-redux?

众所周知,react-redux是一个桥接react和redux的类库。基本实现原理是通过消费redux core 所提供的 store.subscribe(listener) 这个 API 来完成的:即监听redux store的state变化,一旦它发生变化了,那么我么就通知 UI 层(react 组件)去重新渲染自己。虽然说,redux core 可以绑定到任何的 UI 层,但是目前为止,大家谈论到 redux 的时候都是默认指的绑定到用 react.js 实现的 UI 层。所以,在这种语境下,一个 redux 框架的最小集合是包括“react.js”,"redux.js","react-redux.js",而react-redux是这个框架的不可或缺的重要一环。

三者之间有什么区别

在探究三者之间有什么区别之前,我们不妨明确一下什么是“状态管理(state management)”。而在明确什么是“状态管理”之前需要明确一下什么是“状态”?

什么是状态呢?“状态”就是一些描述应用行为的动态数据。如果对前端状态进行区分的话,那么我们又可以分为“业务状态”和“UI 状态”,“本地状态” 和 “共享状态”等。如何划分状态不重要,重要的是“状态”是一个关于如何存储,读取,更新数据的一个话题。

状态机专家,XState.js类库作者 David Khourshid 说过:

State management is how state changes over time.

如果说实现了状态管理的集合代码就是“状态管理器”的话,综上所述,状态管理器需要满足以下要求:

  • 能够存储和初始化状态值
  • 能够读取到现在的状态值
  • 能够更新状态值并且通知到外界

现在,从这个角度来看看,上面提到的两个技术(Context API,Context API + useReducer)跟redux + react-redux有什么不同。

  • Context API

    • 这个技术目的是解决所谓的 “prop-drilling” 问题,实现了数据的【传递】和【共享】而已。它并不是真正的状态管理器。
    • 在 React DevTools 中显示 Provider 和 Consumer 组件的当前context值,但不显示该值随时间变化的任何历史记录
    • 订阅了context实例对象的组件在context值变化的时候一定会被强制更新,也就是说没办法跳过更新。
    • 不包括任何副作用和中间件机制。
  • Context API + useReducer

    • Context API 有点不足点,它一并具有

    • 某种程度上是一个状态管理器,但是功能要比redux + react-redux要弱,比单纯的Context API要强。

    • Context API + useReducer实现的redux范式没有副作用机制和中间件的扩展能力。当然,你可以在react hook 的能力上去封装出对应的副作用能力,中间件能力,selector能力,但是如果是那样的话,你只不过在实现在一个低配版的redux而已。

    • 在这种范式下,只要你的组件订阅了context实例对象,那么一旦context对象的某部分值发生改变,你的组件都会被迫更新,即使你没有消费这部分的值。但是在 redux+ react-redux 的实现里面就不会这样。只有当前组件所消费的状态值发生改变了,当前组件才会被更新。这就避免了因为不必要更新所带来的性能问题。

    • 在周边工具这一块,Context API + useReducer有React DevTools,而redux+ react-redux 有 Redux DevTools。从撞他管理的角度,Redux DevTools比React DevTools强多了,它能够回答关于状态管理的四个“什么”问题:状态在“什么时候”,“在什么地方”,“为什么”,“发生了什么样的”变化。

各自适用的场景

还是那句老话,脱离具体的业务场景去讨论不同技术的好坏都是耍流氓。那些相似的技术就像世间上的各种“刀”,一旦发明出来,则意味着必定有它的最佳使用场景。下面我们来总结一下这三种技术的各自适用场景分别是什么。

  • Context API
    • 只考虑偏静态数据的跨组件层级传递和共享,不考虑状态更新
  • Context API + useReducer
    • 小中型的状态管理场景,意味着状态规模不大,更新状态的逻辑代码不复杂
    • 对代码的可调试性没有很高的要求
    • 对渲染性能没有很高的要求
  • redux + react-redux
    • 中大型的状态管理场景,意味着状态规模很大,更新状态的逻辑代码比较复杂,存在多人协作
    • 代码的可调式性有比较高的要求。需要快速地知道四个“什么”问题(状态在“什么时候”,“在什么地方”,“为什么”,“发生了什么样的”变化)的答案。
    • 需要用到副作用机制增加代码的可读性
    • 需要用到中间件来增加应用的可扩展性
    • 需要对用户操作轨迹进行日志记录
    • 考虑使用一个redux store 对接到不同的 UI 层(比如同时对接到react和vue)
    • 在渲染性能上有更高的追求

可能你会觉得在什么时候采用Context API + useReducer与什么时候采用redux + react-redux的抉择上依然感到困惑,那不妨考虑采纳 redux core 团队成员Mark Erikson的观点:

if you get past 2-3 state-related contexts in an application, you're re-inventing a weaker version of React-Redux and should just switch to using Redux.

也就是说,当你的react应用开始出现三个或者以上的context对象,你是时候要考虑切换到redux框架上来了。

总结

时至今日,redux 死了没?我的答案是:“它还活着”。只要前端开发范式没有变,还是【数据驱动DOM更新】的话,我相信,redux 这种状态管理框架不但能活着,而且它还会在中大型的前端项目中继续大放异彩。

参考资料

-Blogged Answers: Why React Context is Not a "State Management" Tool (and Why It Doesn't Replace Redux)

文章分类
前端
文章标签