精读《unstated 与 unstated-next 源码》

1 引言

unstated 是基于 Class Component 的数据流管理库,unstated-next 是针对 Function Component 的升级版,且特别优化了对 Hooks 的支持。前端

与类 redux 库相比,这个库设计的别出心裁,并且这两个库源码行数都特别少,与 180 行的 unstated 相比,unstated-next 只有不到 40 行,但想象空间却更大,且用法符合直觉,因此本周精读就会从用法与源码两个角度分析这两个库。react

2 概述

首先问,什么是数据流?React 自己就提供了数据流,那就是 setStateuseState,数据流框架存在的意义是解决跨组件数据共享与业务模型封装。git

还有一种说法是,React 早期声称本身是 UI 框架,不关心数据,所以须要生态提供数据流插件弥补这个能力。但其实 React 提供的 createContextuseContext 已经能解决这个问题,只是使用起来稍显麻烦,而 unstated 系列就是为了解决这个问题。github

unstated

unstated 解决的是 Class Component 场景下组件数据共享的问题。redux

相比直接抛出用法,笔者还原一下做者的思考过程:利用原生 createContext 实现数据流须要两个 UI 组件,且实现方式冗长:api

const Amount = React.createContext(1);

class Counter extends React.Component {
  state = { count: 0 };
  increment = amount => {
    this.setState({ count: this.state.count + amount });
  };
  decrement = amount => {
    this.setState({ count: this.state.count - amount });
  };
  render() {
    return (
      <Amount.Consumer>
        {amount => (
          <div>
            <span>{this.state.count}</span>
            <button onClick={() => this.decrement(amount)}>-</button>
            <button onClick={() => this.increment(amount)}>+</button>
          </div>
        )}
      </Amount.Consumer>
    );
  }
}

class AmountAdjuster extends React.Component {
  state = { amount: 0 };
  handleChange = event => {
    this.setState({
      amount: parseInt(event.currentTarget.value, 10)
    });
  };
  render() {
    return (
      <Amount.Provider value={this.state.amount}>
        <div>
          {this.props.children}
          <input
            type="number"
            value={this.state.amount}
            onChange={this.handleChange}
          />
        </div>
      </Amount.Provider>
    );
  }
}

render(
  <AmountAdjuster>
    <Counter />
  </AmountAdjuster>
);
复制代码

而咱们要作的,是将 setState 从具体的某个 UI 组件上剥离,造成一个数据对象实体,能够被注入到任何组件。promise

这就是 unstated 的使用方式:微信

import React from "react";
import { render } from "react-dom";
import { Provider, Subscribe, Container } from "unstated";

class CounterContainer extends Container {
  state = {
    count: 0
  };

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

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

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

render(
  <Provider> <Counter /> </Provider>,
  document.getElementById("root")
);
复制代码

首先要为 Provider 正名:Provider 是解决单例 Store 的最佳方案,当项目与组件都是用了数据流,须要分离做用域时,Provider 便派上了用场。若是项目仅需单 Store 数据流,那么与根节点放一个 Provider 等价。app

其次 CounterContainer 成为一个真正数据处理类,只负责存储与操做数据,经过 <Subscribe to={[CounterContainer]}> RenderProps 方法将 counter 注入到 Render 函数中。框架

unstated 方案本质上利用了 setState,但将 setState 与 UI 剥离,并能够很方便的注入到任何组件中。

相似的是,其升级版 unstated-next 本质上利用了 useState,利用了自定义 Hooks 能够与 UI 分离的特性,加上 useContext 的便捷性,利用不到 40 行代码实现了比 unstated 更强大的功能。

unstated-next

unstated-next 用 40 行代码号称 React 数据管理库的终结版,让咱们看看它是怎么作到的!

仍是从思考过程提及,笔者发现其 README 也提供了对应思考过程,就以其 README 里的代码做为案例。

首先,使用 Function Component 的你会这样使用数据流:

function CounterDisplay() {
  let [count, setCount] = useState(0);
  let decrement = () => setCount(count - 1);
  let increment = () => setCount(count + 1);
  return (
    <div> <button onClick={decrement}>-</button> <p>You clicked {count} times</p> <button onClick={increment}>+</button> </div>
  );
}
复制代码

若是想将数据与 UI 分离,利用 Custom Hooks 就能够完成,这不须要借助任何框架:

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

function CounterDisplay() {
  let counter = useCounter();
  return (
    <div> <button onClick={counter.decrement}>-</button> <p>You clicked {counter.count} times</p> <button onClick={counter.increment}>+</button> </div>
  );
}
复制代码

若是想将这个数据分享给其余组件,利用 useContext 就能够完成,这不须要借助任何框架:

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

let Counter = createContext(null);

function CounterDisplay() {
  let counter = useContext(Counter);
  return (
    <div> <button onClick={counter.decrement}>-</button> <p>You clicked {counter.count} times</p> <button onClick={counter.increment}>+</button> </div>
  );
}

function App() {
  let counter = useCounter();
  return (
    <Counter.Provider value={counter}> <CounterDisplay /> <CounterDisplay /> </Counter.Provider> ); } 复制代码

但这样仍是显示使用了 useContext 的 API,而且对 Provider 的封装没有造成固定模式,这就是 usestated-next 要解决的问题。

因此这就是 unstated-next 的使用方式:

import { createContainer } from "unstated-next";

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

let Counter = createContainer(useCounter);

function CounterDisplay() {
  let counter = Counter.useContainer();
  return (
    <div> <button onClick={counter.decrement}>-</button> <p>You clicked {counter.count} times</p> <button onClick={counter.increment}>+</button> </div>
  );
}

function App() {
  return (
    <Counter.Provider> <CounterDisplay /> <CounterDisplay /> </Counter.Provider> ); } 复制代码

能够看到,createContainer 能够将任何 Hooks 包装成一个数据对象,这个对象有 ProvideruseContainer 两个 API,其中 Provider 用于对某个做用域注入数据,而 useContainer 能够取到这个数据对象在当前做用域的实例。

对 Hooks 的参数也进行了规范化,咱们能够经过 initialState 设定初始化数据,且不一样做用域能够嵌套并赋予不一样的初始化值:

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

const Counter = createContainer(useCounter);

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

function App() {
  return (
    <Counter.Provider>
      <CounterDisplay />
      <Counter.Provider initialState={2}>
        <div>
          <div>
            <CounterDisplay />
          </div>
        </div>
      </Counter.Provider>
    </Counter.Provider>
  );
}
复制代码

能够看到,React Hooks 已经很是适合作状态管理,而生态应该作的事情是尽量利用其能力进行模式化封装。

有人可能会问,取数和反作用怎么办?redux-saga 和其余中间件都没有,这个数据流是否是阉割版?

首先咱们看 Redux 为何须要处理反作用的中间件。这是由于 reducer 是一个同步纯函数,其返回值就是操做结果中间不能有异步,且不能有反作用,因此咱们须要一种异步调用 dispatch 的方法,或者一个反作用函数来存放这些 “脏” 逻辑。

而在 Hooks 中,咱们能够随时调用 useState 提供的 setter 函数修改值,这早已自然解决了 reducer 没法异步的问题,同时也实现了 redux-chunk 的功能。

而异步功能也被 useEffect 这个 React 官方 Hook 替代。咱们看到这个方案能够利用 React 官方提供的能力彻底覆盖 Redux 中间件的能力,对 Redux 库实现了降维打击,因此下一代数据流方案随着 Hooks 的实现是真的存在的

最后,相比 Redux 自身以及其生态库的理解成本(笔者不才,初学 Redux 以及其周边 middleware 时理解了很久),Hooks 的理解学习成本明显更小。

不少时候,人们排斥一个新技术,并非由于新技术很差,而是这可能让本身多年精通的老手艺带来的 “竞争优点” 彻底消失。可能一个织布老专家手工织布效率是入门学员的 5 倍,但换上织布机器后,这个差别很快会被抹平,老织布专家面临被淘汰的危机,因此维护这份老手艺就是维护他本身的利益。但愿每一个团队中的老织布工人都能主动引入织布机。

再看取数中间件,咱们通常须要解决 取数业务逻辑封装取数状态封装,经过 redux 中间件能够封装在内,经过一个 dispatch 解决。

其实 Hooks 思惟下,利用 swr useSWR 同样能解决:

function Profile() {
  const { data, error } = useSWR("/api/user");
}
复制代码

取数的业务逻辑封装在 fetcher 中,这个在 SWRConfigContext.Provider 时就已注入,还能够控制做用域!彻底利用 React 提供的 Context 能力,能够感觉到实现底层原理的一致性和简洁性,越简单越优美的数学公式越多是真理。

而取数状态已经封装在 useSWR 中,配合 Suspense 能力,连 Loading 状态都不用关心了。

3 精读

unstated

咱们再梳理一下 unstated 这个库作了哪些事情。

  1. 利用 Provider 申明做用范围。
  2. 提供 Container 做为能够被继承的类,继承它的 Class 做为 Store。
  3. 提供 Subscribe 做为 RenderProps 用法注入 Store,注入的 Store 实例由参数 to 接收到的 Class 实例决定。

对于第一点,Provider 在 Class Component 环境下要初始化 StateContext,这样才能在 Subscribe 中使用:

const StateContext = createReactContext(null);

export function Provider(props) {
  return (
    <StateContext.Consumer>
      {parentMap => {
        let childMap = new Map(parentMap);

        if (props.inject) {
          props.inject.forEach(instance => {
            childMap.set(instance.constructor, instance);
          });
        }

        return (
          <StateContext.Provider value={childMap}>
            {props.children}
          </StateContext.Provider>
        );
      }}
    </StateContext.Consumer>
  );
}
复制代码

对于第二点,对于 Container,须要提供给 Store setState API,按照 React 的 setState 结构实现了一遍。

值得注意的是,还存储了一个 _listeners 对象,而且可经过 subscribeunsubscribe 增删。

_listeners 存储的实际上是当前绑定的组件 onUpdate 生命周期,而后在 setState 时主动触发对应组件的渲染。onUpdate 生命周期由 Subscribe 函数提供,最终调用的是 this.setState,这个在 Subscribe 部分再说明。

如下是 Container 的代码实现:

export class Container<State: {}> {
  state: State;
  _listeners: Array<Listener> = [];

  constructor() {
    CONTAINER_DEBUG_CALLBACKS.forEach(cb => cb(this));
  }

  setState(
    updater: $Shape<State> | ((prevState: $Shape<State>) => $Shape<State>),
    callback?: () => void
  ): Promise<void> {
    return Promise.resolve().then(() => {
      let nextState;

      if (typeof updater === "function") {
        nextState = updater(this.state);
      } else {
        nextState = updater;
      }

      if (nextState == null) {
        if (callback) callback();
        return;
      }

      this.state = Object.assign({}, this.state, nextState);

      let promises = this._listeners.map(listener => listener());

      return Promise.all(promises).then(() => {
        if (callback) {
          return callback();
        }
      });
    });
  }

  subscribe(fn: Listener) {
    this._listeners.push(fn);
  }

  unsubscribe(fn: Listener) {
    this._listeners = this._listeners.filter(f => f !== fn);
  }
}
复制代码

对于第三点,Subscriberender 函数将 this.props.children 做为一个函数执行,并把对应的 Store 实例做为参数传递,这经过 _createInstances 函数实现。

_createInstances 利用 instanceof 经过 Class 类找到对应的实例,并经过 subscribe 将本身组件的 onUpdate 函数传递给对应 Store 的 _listeners,在解除绑定时调用 unsubscribe 解绑,防止没必要要的 renrender。

如下是 Subscribe 源码:

export class Subscribe<Containers: ContainersType> extends React.Component<
  SubscribeProps<Containers>,
  SubscribeState
> {
  state = {};
  instances: Array<ContainerType> = [];
  unmounted = false;

  componentWillUnmount() {
    this.unmounted = true;
    this._unsubscribe();
  }

  _unsubscribe() {
    this.instances.forEach(container => {
      container.unsubscribe(this.onUpdate);
    });
  }

  onUpdate: Listener = () => {
    return new Promise(resolve => {
      if (!this.unmounted) {
        this.setState(DUMMY_STATE, resolve);
      } else {
        resolve();
      }
    });
  };

  _createInstances(
    map: ContainerMapType | null,
    containers: ContainersType
  ): Array<ContainerType> {
    this._unsubscribe();

    if (map === null) {
      throw new Error(
        "You must wrap your <Subscribe> components with a <Provider>"
      );
    }

    let safeMap = map;
    let instances = containers.map(ContainerItem => {
      let instance;

      if (
        typeof ContainerItem === "object" &&
        ContainerItem instanceof Container
      ) {
        instance = ContainerItem;
      } else {
        instance = safeMap.get(ContainerItem);

        if (!instance) {
          instance = new ContainerItem();
          safeMap.set(ContainerItem, instance);
        }
      }

      instance.unsubscribe(this.onUpdate);
      instance.subscribe(this.onUpdate);

      return instance;
    });

    this.instances = instances;
    return instances;
  }

  render() {
    return (
      <StateContext.Consumer>
        {map =>
          this.props.children.apply(
            null,
            this._createInstances(map, this.props.to)
          )
        }
      </StateContext.Consumer>
    );
  }
}
复制代码

总结下来,unstated 将 State 外置是经过自定义 Listener 实现的,在 Store setState 时触发收集好的 Subscribe 组件的 rerender。

unstated-next

unstated-next 这个库只作了一件事情:

  1. 提供 createContainer 将自定义 Hooks 封装为一个数据对象,提供 Provider 注入与 useContainer 获取 Store 这两个方法。

正如以前解析所说,unstated-next 可谓将 Hooks 用到了极致,认为 Hooks 已经彻底具有数据流管理的所有能力,咱们只要包装一层规范便可:

export function createContainer(useHook) {
  let Context = React.createContext(null);

  function Provider(props) {
    let value = useHook(props.initialState);
    return <Context.Provider value={value}>{props.children}</Context.Provider>;
  }

  function useContainer() {
    let value = React.useContext(Context);
    if (value === null) {
      throw new Error("Component must be wrapped with <Container.Provider>");
    }
    return value;
  }

  return { Provider, useContainer };
}
复制代码

可见,Provider 就是对 value 进行了约束,固化了 Hooks 返回的 value 直接做为 value 传递给 Context.Provider 这个规范。

useContainer 就是对 React.useContext(Context) 的封装。

真的没有其余逻辑了。

惟一须要思考的是,在自定义 Hooks 中,咱们用 useState 管理数据仍是 useReducer 管理数据的问题,这个是个仁者见仁的问题。不过咱们能够对自定义 Hooks 进行嵌套封装,支持一些更复杂的数据场景,好比:

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

function useUser(initialState = {}) {
  const [name, setName] = useState(initialState.name);
  const [age, setAge] = useState(initialState.age);
  const registerUser = userInfo => {
    setName(userInfo.name);
    setAge(userInfo.age);
  };
  return { user: { name, age }, registerUser };
}

function useApp(initialState) {
  const { count, decrement, increment } = useCounter(initialState.count);
  const { user, registerUser } = useUser(initialState.user);
  return { count, decrement, increment, user, registerUser };
}

const App = createContainer(useApp);
复制代码

4 总结

借用 unstated-next 的标语:“never think about React state management libraries ever again” - 用了 unstated-next 不再要考虑其余 React 状态管理库了。

而有意思的是,unstated-next 自己也只是对 Hooks 的一种模式化封装,Hooks 已经能很好解决状态管理的问题,咱们真的不须要 “再造” React 数据流工具了。

讨论地址是:精读《unstated 与 unstated-next 源码》 · Issue #218 · dt-fe/weekly

若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证

相关文章
相关标签/搜索