结合具体场景,聊聊 React 的状态管理方案

1. 引子

虽然 React 的状态管理是一个老生常谈的问题,网上和社区中也能搜到至关多的资料。这里仍是想梳理下从我接触 React 开始到如今对状态管理的一些感想。前端

全部的新技术的出现和流行都是为了解决特定的场景问题,这里也会以一个很是简单的例子做为咱们故事的开始。react

有这样一个需求,咱们须要在界面上展现某个商品的信息,可能咱们会这样实现:程序员

import React, { PureComponent } from 'react';

export default class ProductInfo extends PureComponent {
 constructor(props) {
    super(props);
    this.state = {
      data: {
        sku: '',
        desc: '',
      },
    };
  }
  
  componentDidMount() {
    fetch('url', { id: this.props.id })
      .then(resp => resp.json())
      .then(data => this.setState({ data }));
  }

  render() {
    const { sku } = this.state.data;
    return (
      <div>{sku}</div>
    );
  }
}

上述的场景虽然很是简单,可是在咱们实际的需求开发中很是常见,采用上述的方式也能很好地解决这一类问题。json

咱们把场景变得稍微复杂一点,假如界面上有两个部分都须要展现商品的信息,只是展现的商品的属性不一样而已,怎么处理了?咱们也能够像上面那样再写一个相似的组件,可是问题是咱们重复获取了同一个商品的信息,为了不重复获取数据,那么咱们就须要在两个组件之间共享商品信息。redux

2. props 解决数据共享

经过 props 解决数据共享问题,本质上是将数据获取的逻辑放到组件的公共父组件中。代码多是这样的:后端

import React, { PureComponent } from 'react';

export default class App extends PureComponent {
 constructor(props) {
    super(props);
    this.state = {
      data: {
        sku: '',
        desc: '',
      },
    };
  }
  
  componentDidMount() {
    fetch('url', { id: this.props.id })
      .then(resp => resp.json())
      .then(data => this.setState({ data }));
  }

  render() {
    return (
      <div>
        <ProductInfoOne data={this.state.data} />
        <ProductInfoTwo data={this.state.data} />
      </div>
    );
  }
}

function ProductInfoOne({ data }) {
  const { sku } = data;
  return <div>{sku}</div>;
}

function ProductInfoTwo({ data }) {
  const { desc } = data;
  return <div>{desc}</div>;
}

对于这种组件嵌套层次只有 一、2 层的场景,经过将数据获取和存储的逻辑上移到公共的父组件就能够很好地解决。数组

可是若是界面呈现更加复杂一点,好比 ProductInfoOne 的子组件中也须要呈现商品的信息,咱们可能会想到继续经过 props 向下传递数据,问题是随着嵌套的层次愈来愈深,数据须要从最外层一直传递到最里层,整个代码的可读性和维护性会变差。咱们但愿打破数据「层层传递」而子组件也能取到父辈组件中的数据。缓存

3. Context API

React 16.3 的版本引入了新的 Context API,Context API 自己就是为了解决嵌套层次比较深的场景中数据传递的问题,看起来很是适合解决咱们上面提到的问题。咱们尝试使用 Context API 来解决咱们的问题:性能优化

// context.js
const ProductContext = React.createContext({
  sku: '',
  desc: '',
});

export default ProductContext;

// App.js
import React, { PureComponent } from 'react';
import ProductContext from './context';

const Provider = ProductContext.Provider;

export default class App extends PureComponent {
 constructor(props) {
    super(props);
    this.state = {
      data: {
        sku: '',
        desc: '',
      },
    };
  }

  componentDidMount() {
    fetch('url', { id: this.props.id })
      .then(resp => resp.json())
      .then(data => this.setState({ data }));
  }

  render() {
    return (
      <Provider value={this.state.data}>
        <ProductInfoOne />
        <ProductInfoTwo />
      </Provider>
    );
  }
}

// ProductInfoOne.js
import React, { PureComponent } from 'react';
import ProductContext from './context';

export default class ProductInfoOne extends PureComponent {
  static contextType = ProductContext;

  render() {
    const { sku } = this.context;
    return <div>{sku}</div>;
  }
}

// ProductInfoTwo.js
import React, { PureComponent } from 'react';
import ProductContext from './context';

export default class ProductInfoTwo extends PureComponent {
  static contextType = ProductContext;

  render() {
    const { desc } = this.context;
    return <div>{desc}</div>;
  }
}

看起来一切都很美好,到目前为止咱们也只是使用了 React 库自己的功能,并无引入任何第三方的库,实际上对于这类比较简单的场景,使用以上的方式来解决确实是最直接、简单的方案。服务器

现实中的需求每每要稍微复杂点,上述的几个场景中咱们偏重于信息的呈现,而真实场景中咱们避免不了一些交互的操做,好比咱们须要在呈现商品信息的同时还须要能够编辑商品的信息,因为 ProductInfoOne、ProductInfoTwo 是受控组件,而且数据源在 App 组件中,为了实现数据的修改,咱们可能经过 Context API 传递修改数据的「回调函数」。

上述的几个场景中咱们偏重于有嵌套关系的组件之间数据的共享,若是场景再复杂一点,假设平行组件之间须要共享数据,例如和 App 没有父子关系的 App1 组件也须要呈现商品信息,怎么办,看起来 Conext API 也是一筹莫展。

4. Redux

终于到了 Redux,相信不少读者以为啰里啰嗦,可是本着技术方案是为了解决特定问题的原则,仍是以为有必要作一些铺垫,若是你的问题场景没有复杂到 React 自己没有太好的解决方式的地步,建议也不要引入额外的技术(有更好的解决方案除外),包括 Redux。

Redux 确实是很强大,目前在 React 状态管理中也仍是最活跃和使用最广的解决方案。这里仍是引用一张图(图片来源)来简单说明下 Redux 解决问题的思路:

这里不想讲太多 Redux 的概念和原理,网上也是一大推资料,相信不少人也对 Redux 很是熟悉了。先看看采用 Redux 解决咱们上述问题,代码大概是这样的(只列出部分重点代码):

// store.js
import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(reducer);

export default store;

// reducer.js
import * as actions from './actions';
import { combineReducers } from 'redux';

function ProductInfo(state = {}, action) {
  switch (action.type) {
    case actions.SET_SKU: {
      return { ...state, sku: action.sku };
    }
    case actions.SET_DESC: {
      return { ...state, desc: action.desc };
    }
    case actions.SET_DATA: {
      return { ...state, ...action.data };
    }
    default: {
      return state;
    }
  }
}

const reducer = combineReducers({
  ProductInfo,
});

export default reducer;

// action.js
export const SET_SKU = 'SET_SKU';
export const SET_DESC = 'SET_DESC';
export const SET_DATA = 'SET_DATA';

export function setSku(sku) {
  return {
    type: SET_SKU,
    sku,
  };
}

export function setDesc(desc) {
  return {
    type: SET_DESC,
    desc,
  };
}

export function setData(data) {
  return {
    type: SET_DESC,
    data,
  };
}

// App.js
import React, { PureComponent } from 'react';
import { Provider } from 'react-redux';
import store from './store';
import * as actions from './actions';

class App extends PureComponent {
  componentDidMount() {
    fetch('url', { id: this.props.id })
      .then(resp => resp.json())
      .then(data => this.props.dispatch(actions.setData(data)));
  }

  render() {
    return (
      <Provider store={store}>
        <ProductInfoOne />
        <ProductInfoTwo />
      </Provider>
    );
  }
}

function mapStateToProps() {
  return {

  };
}

function mapDispatchToProps(dispatch) {
  return {
    dispatch,
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(App);

// ProductInfoOne.js
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import * as actions from './actions';

class ProductInfoOne extends PureComponent {
  onEditSku = (sku) => {
    this.props.dispatch(actions.setSku(sku));
  };

  render() {
    const { sku } = this.props.data;
    return (
      <div>{sku}</div>
    );
  }
}

function mapStateToProps(state) {
  return {
    data: state.ProductInfo,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    dispatch,
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(ProductInfoOne);

// ProductInfoTwo.js
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import * as actions from './actions';

class ProductInfoTwo extends PureComponent {
  onEditDesc = (desc) => {
    this.props.dispatch(actions.setDesc(desc));
  };

  render() {
    const { desc } = this.props.data;
    return (
      <div>{desc}</div>
    );
  }
}

function mapStateToProps(state) {
  return {
    data: state.ProductInfo,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    dispatch,
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(ProductInfoTwo);

Redux 确实可以解决咱们上面提到的问题,从代码和 Redux 的原理中咱们也能够知道,Redux 作了不少概念的抽象和分层,store 专门负责数据的存储,action 用于描述数据修改的动做,reducer 用于修改数据。咋一看,Redux 使咱们的代码变得更加复杂了,可是它抽象出来的这些概念和一些强制的规定,会让数据的共享和修改变得有迹可循,这种约定的规则,在多人协助开发的大型项目中,会让代码的逻辑更加清晰、可维护性更好。

可是,Redux 被你们诟病的地方也不少,网上也有愈来愈多对 Redux 批判的声音,暂且不谈技术的学习成本,笔者在使用过程当中以为有几点让人抓狂的地方:

  • 对于「简单」系统来讲太啰嗦了,笔者所负责的系统是偏向中后台系统,系统自己也不复杂,而且是一我的负责开发,为了修改某个数据,须要修改多个文件;过一段时间再去看某个数据变更的逻辑,须要将整个数据变更的流程过一遍,不够直接。尤为是须要处理一些异步操做时,还须要引入一些反作用处理库,例如 redux-thunk、redux-saga、redux-observables,这样反而会致使一个简单的系统更加复杂,有一种「杀鸡焉用牛刀」的感受。
  • 数据缓存问题,Redux 中 store 是全局惟一的对象,不会随着某个组件的消亡而消亡。这个问题须要辩证来看,在须要缓存数据的场景中,Redux 自然就支持;可是在某些不须要缓存的场景中,可能会带来很是严重的后果,好比笔者负责开发的一个商品交易页面,每次跳转到该页面时会获取商品的信息并存到 store 中,若是某次获取商品信息的部分接口失败,那么会致使 store 中存放的部分商品信息是缓存的上次购买的商品信息,这样会致使界面呈现的商品信息是错误的。对于这种场景咱们还须要额外有一段代码去处理 store 中缓存的数据,要么在组件销毁的时候清空对应的缓存,要么在获取数据前或者获取数据失败的函数中处理 store 中的缓存。

那么有没有一些更加轻量级的状态管理库了?

5. MobX

Mobx 从 2016 年开始发布第一个版本,到如今短短两年多的时间,发展也是很是迅速,受到愈来愈多人的关注。MobX 的实现思路很是简单直接,相似于 Vue 中的响应式的原理,其实质能够简单理解为观察者模式,数据是被观察的对象,「响应」是观察者,响应能够是计算值或者函数,当数据发生变化时,就会通知「响应」执行。借用一张网上的图(图片来源)描述下原理:

Mobx 我理解的最大的好处是简单、直接,数据发生变化,那么界面就从新渲染,在 React 中使用时,咱们甚至不须要关注 React 中的 state,咱们看下用 MobX 怎么解决咱们上面的问题:

// store.js
import { observable } from 'mobx';

const store = observable({
  sku: '',
  desc: '',
});
export default store;

// App.js
import React, { PureComponent } from 'react';
import store from './store.js';

export default class App extends PureComponent {
  componentDidMount() {
    fetch('url', { id: this.props.id })
      .then(resp => resp.json())
      .then(data => Object.assign(store, data));
  }

  render() {
    return (
      <div>
        <ProductInfoOne />
        <ProductInfoTwo />
      </div>
    );
  }
}

// ProductInfoOne.js
import React, { PureComponent } from 'react';
import { action } from 'mobx';
import { observer } from 'mobx-react';
import store from './store';

@observer
class ProductInfoOne extends PureComponent {
  @action
  onEditSku = (sku) => {
    store.sku = sku;
  };

  render() {
    const { sku } = store;
    return (
      <div>{sku}</div>
    );
  }
}

export default ProductInfoOne;

// ProductInfoTwo.js
import React, { PureComponent } from 'react';
import { action } from 'mobx';
import { observer } from 'mobx-react';
import store from './store';

@observer
class ProductInfoTwo extends PureComponent {
  @action
  onEditDesc = (desc) => {
    store.desc = desc;
  };

  render() {
    const { desc } = store;
    return (
      <div>{desc}</div>
    );
  }
}

export default ProductInfoTwo;

稍微解释下用到的新的名词,observable 或者 @observable 表示声明一个可被观察的对象,@observer 标识观察者,其本质是将组件中的 render 方法用 autorun 包装了下,@action 描述这是一个修改数据的动做,这个注解是可选的,也就是不用也是能够的,可是官方建议使用,这样代码逻辑更清晰、底层也会作一些性能优化、而且在调试的时候结合调试工具可以提供有用的信息。

咱们能够对比下 Redux 的方案,使用 MobX 后代码大大减小,而且数据流动和修改的逻辑更加直接和清晰。声明一个可被观察的对象,使用 @observer 将组件中的 render 函数变成观察者,数据修改直接修改对象的属性,咱们须要作的就是这些。

可是从中也能够看到,Mobx 的数据修改说的好听点是「灵活」,很差听点是「随意」,好在社区有一些其余的库来优化这个问题,好比 mobx-state-tree 将 action 在模型定义的时候就肯定好,将修改数据的动做集中在一个地方管理。不过相对于 Redux 而言,Mobx 仍是灵活不少,它没有太多的约束和规则,在少许开发人员或者小型项目中,会很是地自由和高效,可是随着项目的复杂度和开发人员的增长,这种「无约束」反而可能会带来后续高昂的维护成本,反之 Redux 的「约束」会确保不一样的人写出来的代码几乎是一致的,由于你必须按照它约定的规则来开发,代码的一致性和可维护性也会更好。

6. GraphQL

前面提到的不论是 Redux 仍是 MobX, 二者都是侧重于管理数据,说的更明白点就是怎样存储、更新数据,可是数据是从哪里来的,它们是不关注的。那么将来有没有一种新的思路来管理数据了,GraphQL 其实提出了一种新的思路。

咱们开发一个组件或者前端系统的时候,有一部分的数据是来自于后台的,好比上面场景中的商品信息,有一部分是来自于前台的,好比对话框是否弹出的状态。GraphQL 将远程的数据和本地的数据进行了统一,让开发者感受到全部的数据都是查询出来的,至因而从服务端查询仍是从本地查询,开发人员不须要关注。

这里不讲解 GraphQL 的具体原理和使用,你们有兴趣能够去查看官网的资料。咱们看看若是采用 GraphQL 来解决咱们上面的问题,代码会是怎么样的?

// client.js
import ApolloClient from 'apollo-boost';

const client = new ApolloClient({
  uri: 'http://localhost:3011/graphql/productinfo'
});

export default client;

// app.js
import React from 'react';
import { ApolloProvider, Query, Mutation } from 'react-apollo';
import gql from 'graphql-tag';
import client from './index';
import ProductInfoOne from './ProductInfoOne';
import ProductInfoTwo from './ProductInfoTwo';

const GET_PRODUCT_INFO = gql`
  query ProductInfo($id: Int) {
    productInfo(id: $id){
      id
      sku
      desc
    }
  }
`;
export default class App extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      id: 1,
    };
  }

  render() {
    return (
      <ApolloProvider client={client}>
        <Query query={GET_PRODUCT_INFO} variables={{ id: this.state.id }}>
          {({ loading, error, data }) => {
            if (loading) return 'loading...';
            if (error) return 'error...';
            if (data) {
              return (
                <div>
                  <ProductInfoOne data={data.productInfo} />
                  <ProductInfoTwo data={data.productInfo} />
                </div>
              );
            }
            return null;
          }}
        </Query>
      </ApolloProvider>
    );
  }
}

// ProductInfoOne.js
import React from 'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';

const SET_SKU = gql`
  mutation SetSku($id: Int, $sku: String){
    setSku(id: $id, sku: $sku) {
      id
      sku
      desc
    }
  }
`;
export default class ProductInfoOne extends React.PureComponent {
  render() {
    const { id, sku } = this.props.data;
    return (
      <div>
        <div>{sku}</div>
        <Mutation mutation={SET_SKU}>
          {(setSku) => (
            <button onClick={() => { setSku({ variables: { id: id, sku: 'new sku' } }) }}>修改 sku</button>
          )}
        </Mutation>
      </div>
    );
  }
}

// ProductInfoTwo.js
import React from 'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';

const SET_DESC = gql`
  mutation SetDesc($id: Int, $desc: String){
    setDesc(id: $id, desc: $desc) {
      id
      sku
      desc
    }
  }
`;
export default class ProductInfoTwo extends React.PureComponent {
  render() {
    const { id, desc } = this.props.data;
    return (
      <div>
        <div>{desc}</div>
        <Mutation mutation={SET_DESC}>
          {(setDesc) => (
            <button onClick={() => { setDesc({ variables: { id: id, desc: 'new desc' } }) }}>修改 desc</button>
          )}
        </Mutation>
      </div>
    );
  }
}

咱们能够看到,GraphQL 将数据封装成 Query 的 GraphQL 语句,将数据的更新封装成了 Mutation 的 GraphQL 语句,对开发者来说,我须要数据,因此我须要一个 Query 的查询,我须要更新数据,因此我须要一个 Mutation 的动做,数据既能够来自于远端服务器也能够来自于本地。

使用 GraphQL 最大的问题是,须要服务器端支持 GraphQL 的接口,才能真正发挥它的威力,虽然如今主流的几种 Web 服务器端语言,好比 Java、PHP、Python、JavaScript,均有对应的实现版本,可是将已有的系统整改成支持 GraphQL,成本也是很是大的;而且 GraphQL 的学习成本也不低。

可是 GraphQL 确实相比于传统的状态管理方案,提供了新的思路。咱们和后台人员制定接口时,老是会有一些模糊有争议的灰色地带,好比页面上要展现一个列表,前端程序员的思惟是表格中的一行是一个总体,后台应该返回一个数组,数组中的每一个元素对应的就是表格中的一行,可是后端程序员可能会从数据模型设计上区分动态数据和静态数据,前台应该分别获取动态数据和静态数据,而后再拼装成一行数据。后端程序员的思惟是我有什么,是生产者的视角;前端程序员的思惟是我须要什么,是消费者的视角。可是 GraphQL 会强迫后台人员在开发接口的时候从消费者的视角来制定先后台交互的数据,由于 GraphQL 中的查询参数每每是根据界面呈现推导出来的。这样对前端而言,会减小一部分和后台制定接口的纠纷,同时也会把一部分的工做「转嫁」到后台。

7. 总结
  • 建议优先从 一、二、3 点来解决问题。
  • 在小型项目或者少许开发人员的项目中,能够采用 MobX,效率会更高一点。
  • 大型项目或者多人协助的项目,考虑采用 Redux,后续维护成本更低。
  • GraphQL 重点去学习和理解下它的思路,在我的项目中能够尝试使用。

8. 参考

文章可随意转载,但请保留此 原文连接

很是欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。

相关文章
相关标签/搜索