unstated: 可能是简单状态管理工具中最好的

unstated: 可能是简单状态管理工具中最好的

随着React v16.7.0 alpha被放出来,这几天业界说的都是React Hooks,简单说来,React Hooks的目的就是『消灭class』,让React组件可以彻底用函数形式表达,当然官方会说:『呵呵,我们依然会支持class的哦,只不过给大家一个选择而已。』

React Hooks当然是好东西,但是我觉得Hooks还是没有解决React中的状态管理问题。

React Hooks中的useReducer,太像Redux了,但依然不是Redux,管理的依然是React组件自己的状态,并不能解决两个组件共享state的问题。

import React, {useReducer} from 'react';

const reducer = (state, action) => {
  switch(action.type) {
    case 'inc': return state + 1;
    case 'dec': return state + 1;
  }
  return state;
};

const Counter = () => {
  // 这个count依然局限于只有Counter组件自己才能够访问,不能提供Redux那样让多个组件访问
  const [count, dispatch] = useReducer(reducer, 0);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => dispatch({type: 'inc'})}>+</button>
      <button onClick={() => dispatch({type: 'dec'})}>-</button>
    </div>
  );
};

export default Counter;

React Hooks中的useContext可以不用写层层叠叠的render props来使用Context功能,但是做事的还是Context不是Hooks,如果说能够取代Redux,也是Context取代Redux,并不是Hooks取代Redux。

//以前这么用Context, render props用多了也很烦的
<XXXContext.Consumer>
  {
    ctx => (
       <AnotherContext.Consumer>
          {
             ctx2 => {
                // 使用ctx和ctx2
             }
          }
       </AnotherContext.Consumer>
    )
  }
</XXXContext.Consumer>

Hooks会是React下一步的重头戏,既然React还是没有提供更好的状态管理方法,我们不得不把目光依然移到第三方工具上。

Redux和Mobx依然是重头戏,但是也的确重(zhong4)了一点点,也应该看一看轻量级的工具,很多工具都自己是『最优秀的轻量级状态管理工具』,在我看来,大部分都只是……吹牛逼而已。

且不说『最』字,光是『优秀』两个字,就至少要做到这几点:

  1. 足够简单,别让人看完你的README都不知道怎么回事,别让人要用你的工具每次都要去参照你的示例代码;
  2. 足够通用,很容易造一个解决特定问题的工具,但是要能解决通用的问题,需要抽象得非常好,这可不容易,很多工具把React的Context功能封装了一下来解决一个特别的问题,你不能要求别人照你的思路去解决所有问题,以为别人完全可以直接去用React的Context;
  3. 最重要的一条,足够React!

1和2无需多言,我们就直说最后一条:足够React

这里React是一个形容词,意思就是,框架体现的哲学要和React一致,不会显得很突兀,最好是让开发者看到之后发出惊叹:『啊!React组件状态就该这么管理啊!

坦白说,Redux和Mobx都走得离React哲学有点远,Redux的action和reducer让程序状态更有条理,但是写出来的代码真的啰嗦,为一点鸡皮蒜毛的状态写一大串action和reducer,写多了谁都会烦;Mobx是一个魔法盒子,很神奇地一个状态改变另一个地方状态就自动变了,当状态树复杂的时候,也很难驾驭。

在所有轻量状态管理工具里,我觉得担当得起『最优秀』三个字的,只有unstated

严格说来,Redux和Mobx这两个工具和React没有直接关系,通常要通过react-redux和mobx-react来操纵React状态,这可能也是他们味道不那么React的原因,而unstated完全就是为React设计的,所以先天就没那么羁绊。

还是以React界的HelloWorld——Counter组件为例,使用状态的代码类似这样。

const Counter = () => {
  return (
    <Subscribe to={[CounterContainer]}>
      {
        counter => (
          <div>
            <span>{counter.state.count}</span>
            <button onClick={counter.decrement}>-</button>
            <button onClick={counter.increment}>+</button>
          </div>
        )
      }
    </Subscribe>
  );
};

依然用了一个render props模式,Subscribe 说,我需要订阅一个CounterContainer类型的对象,至于 CounterContainer怎么来的,先不用管,我们只需要知道, 这个CounterContainer 实例(也就是上面代码中通过render props传过来的的counter)应该包含state 状态,这个statecount,同时,这个counter实例还支持incrementdecrement 方法来修改count 值。

很明显,这个CounterContainer是对状态的封装,不光可以读取装填,还提供方法更新状态,这样的抽象对使用它的组件Counter 刚刚好,不多不少。如果用Redux,就需要写action和reducer,还要做dispatch,代码就多了,使用unstated只需要一个函数调用。

再来看CounterContainer怎么实现的。

import {Container} from 'unstated';

class CounterContainer extends Container {
  constructor(initCount) {
    super(...arguments);

    this.state = {count: initCount || 0};
  }

  increment = () => {
    this.setState({count: this.state.count + 1});
  };

  decrement = () => {
    this.setState({count: this.state.count - 1});
  };
};

第一眼看过去,你一定会想:这不就是实现一个React组件吗?

再仔细看,你会发现CounterContainer 继承自Container ,这是unstated提供的一个类,但是整个CounterContainer的代码,真的和一个React组件非常像,一样通过this.state 访问状态,一样通过this.setState更新状态。

实际上setState也可以用上函数式参数,this.state也不会在this.setState之后立刻更新,一句话,React组件具有的,unstated的Container全部具有。

上面的CounterContainer代码可以这么写。

 class CounterContainer extends Container {
  constructor(initCount) {
    super(...arguments);

    this.state = {count: initCount || 0};
  }

  increment = () => {
    this.setState(prevState => {count: prevState.count + 1});
  };

  decrement = () => {
    this.setState(prevState => {count: prevState.count - 1});
  };
};

这就是我所说的—— 足够React。

你在使用unstated的时候,几乎感觉不到是在用一个第三方库,因为它的API和做法和React一脉相承。

再来看如果提供CounterContainer实例,要用上unstated提供的『提供者』(Provider),唉,那么多工具都导出名为Provider的类。

import {Provider} from 'unstated';

const countStore = new CounterContainer(123);

  // JSX
  <Provider inject={[countStore]}>
     {/* 包含Counter的组件树 *}
  </Provider>

利用Provider,就在组件的上下文环境中注入了一个实例,请注意inject属性是一个数组,也就是说可以注入多个不同实例,比如,我们如果这么注入。

  <Provider inject={[countStore, fooStore]}>
     {/* 包含Counter的组件树 *}
  </Provider>

那么,在Subscribe的位置也可以得到多个store。

    <Subscribe to={[CounterContainer, FooContainer]}> 
       {countStore, fooStore) => {
       }
    </Subscribe>

虽然躲不掉还是要用一次render props,但至少不用和React Context一样需要嵌套render props。

代码如此清爽,让人感觉——React的跨组件共享数据就该这么做啊,早就该这样了!

总结比较一下unstated、React自己、Redux和Mobx。

首先,React做跨组件的状态共享给的就是Context方案,但是Context只是一个对象,unstated往前走了一步,让类似Context的Container是一个具备reactive属性的对象,可以被Subscribe,如果重新发明一套规则就没意思了,因为React组件本身就有reactive属性,所以Container就沿用React组件的写法,用this.statethis.setState来管理状态。可以认为,Container就是一个共享状态的React组件

在增强React共享状态的路上,Redux和Mobx走得更远。Redux要action和reducer,unstated把这一套简化为调用setState的函数定义;Mobx走了另一条路,也走得够远的,数据更改靠的是Push,而不是React和Redux用的pull。

所以,unstated居于React原生方案和Redux/Mobx之间,没有离React走得太远,三者在React哲学上的距离可以用下图表示。

不过,unstated终归还是要写class的,Container是class,我们继承Container当然也要写class。到了React Hooks时代,我们不是要消灭class吗?那unstated当如何自处?

嗨,既然unstated秉承了React的哲学,React能够没有class,让unstated没有class,那都不叫事。

unstated的作者Jamie表示下一个版本的unstated就会提供Hooks样式的API

这样一来,unstated实现的Counter代码就会是这样。

function useCounter(initCount = 0) {
  const [count, setCount] = useState(initCount);
  const increment = () => setCount(count => count + 1);
  const decrement = () => setCount(count => count - 1);
  return {count, increment, decrement};
}

function Counter() {
  const counter = useContainer(useCounter);
  return (
    <div>
      <span>{counter.count}</span>
      <button onClick={counter.decrement}>-</button>
      <button onClick={counter.increment}>+</button>
    </div>
  );
}

你看,只要你遵从React的哲学,你就很容易跟着React的进化一起进化。

编辑于 2018-11-23

文章被以下专栏收录