让最简单的 React Hooks 状态管理器 → 更简单一点

2,240 阅读6分钟

1. flooks 是什么?

flooks 是一个 React Hooks 状态管理器,它 1.0 版本的使用方式是这样的:

import { setModel, useModel } from 'flooks';

setModel('demo', {
  state: { a: 0 },
  actions: ({ model, setState }) => ({
    setA() {
      const { b } = model('another');
      setState({ a: b + 1 });
    },
  }),
});

const App = () => {
  const { a, setA } = useModel('demo');
};

虽然它号称最简单(好吧其实就我号称的),但似乎也没有那么简单,粗一看还是不容易一眼看明白。

2. flooks 1.0 有什么问题

  1. 当初为了简化使用,使用了 useModel() 传入字符串的方式,从而无需引入文件获取 model。但重新考虑后,这种方式在开发时,并没有引入文件科学:

    ① 引入文件时,可以很清晰的知道自己在干什么,需要引入哪个文件,

    ② 引入文件利用 IDE 可以快速跳到指定文件,

    ③ 精简是设计目标,但精简不应以牺牲明确为代价,“抽象”在开发时应让位于“清晰”;

  2. setModel() 的 API 设计略显臃肿,虽然 setModel(name, model) 的调用方式足够清晰,但它就是没那么简洁,没那么好看,让人忍不住看两眼就烦;

  3. 重点来自于 model 中 actions 的使用方式,借鉴 rematch 使用函数传参引入功能,这个比 setModel() 的设计更让人崩溃,越看越丑,什么玩意?

  4. model 中分为 state 和 actions 两部分,不够简洁,最重要的是不符合直觉。从直觉来说,不管是 useModel() 还是 model(),理论上拿到的应该是对象第一层 state 和 actions,而非第二层子数据;

  5. model() 与 useModel() 的问题一样,也是字符串传参,经重新考虑,这种方式并没有直接引入文件直观便利。

总之,直觉很重要,要符合直觉。

3. flooks 改进目标

  • 简洁(不能为了简洁牺牲易用,但简洁当然是重要的)
  • 清晰(代码一眼看过去,不会一团乱,不会出现 actions 这种奇怪的东西)
  • 易用(以便于开发为目标,虽有时与简洁冲突,但它们也可以因天下大同的共同愿景而目标一致)

4. 如何改进?

  1. 不使用字符串传参设计,改为直接引入文件;
  2. 改进 setModel()、model() 这样猥琐的 API 设计;
  3. 干掉 state 与 actions,它们并不是必须,也并没有那么重要;
  4. 为世界上最简单的 React Hooks 状态管理器而再坚持一下。

5. flooks 1.0 API 分析

  • setModel() - initializer
  • useModel() - React Hooks
  • model() - model getter (get own model & get other models)
  • setState() - model setter (set own model, can't set others)

基本就是 initializer、getter、setter 再加一个 React Hooks,毕竟是 React Hooks 特供状态管理器。

6. flooks 2.0 如何设计?

先忘掉 1.0,从直觉来设计,model 怎么写最方便:

const counter = {
  count: 0,
  add() {
    const { count } = model(); // get own
    const { num } = model('another'); // get others
    setState({ count: count + num }); // set own
  },
};

是的,这样就够了,有数据,有方法,并不需要 state 和 actions。

上面说了,model() 和 setState() 其实就是 getter 和 setter。我们希望将使用其它 model 也改为文件引入,而非字符串获取,那么:

import another from 'path/to/another.js'

const counter = {
  count: 0,
  add() {
    const { count } = get(); // get own
    const { num } = get(another); // get others
    set({ count: count + 1 }); // set own
  },
};

上面的设计,get(another) 可以再简化吗,比如 another()?少写了三个字母,看起来也更直观。

要想将 another 对象变成可在其它 model 调用的函数 another(),自然需要 initializer 的帮助。现在剩下 initializer 和 React Hooks 两个 API,可以合并成一个吗?

我们似乎并不需要一个专门的 Hooks API,直接让 initializer 返回这个 React Hooks 可以吗?

废话,当然可以,要不然我在这写文章干嘛。

React Hooks 是个函数,another() 也是个函数,那就正好需要 initializer 返回的两种需求都是函数。

前面都叫 get set 了,那 initializer 怎么也得表示一下,就叫 use 吧:

import another from 'path/to/another.js';

const counter = {
  count: 0,
  add() {
    const { count } = get(); // get own
    const { num } = another(); // get others
    set({ count: count + 1 }); // set own
  },
};

export default use(counter); // initializer

实际开发中,很快发现,其实 get 和 set 是可以合并的。就像 jQuery 的 API 设计一样,getter 和 setter 是同一个,比如 $().text() 获取,$().text(content) 设置,非常简洁。

前面都叫 use 了,那 getter、setter 怎么也得表示一下,就叫 now 吧(好吧,now 这个名字其实想了很多的,但还是 now 最贴切)。

实际开发中,很快发现,其实 use 和 now 也是可以合并的。是不是有些过分了?好像违背了清晰的原则。

但如果不实现这个功能,就如鲠在喉、寝食难安的,毕竟它就在那里,不试一下怎么行。

前面这些功能,最大的难点来自于如何区分 use 返回是被当做 React Hooks 调用,还是被当做 model getter 在其它 model 中调用。

因为区分稍有问题,React Hooks 调用错乱就会报错,很崩溃,差点放弃。找来找去 React 似乎没有提供标明当前是处在组件中的 API。

好在最后打滚半天,突然想到了一个解决办法,豁然开朗,感兴趣的可以去看一下实现,看完别骂人,打人也不对。

其次是如何在使用 use 获取与更新 model 时确诊到当前 model,不过这个不是很难。

总之,flooks 最终变成了这样:

import another from 'path/to/another.js';

const counter = {
  count: 0,
  add() {
    const { count } = use(); // get own
    const { num } = another(); // get others
    use({ count: count + 1 }); // set own
  },
};

export default use(counter); // initializer

It's all about use,
use for 3 things,
use to rule them all.

7. 完整示例

下面是一个完整示例,展示了 flooks 2.0 的功能特点:

  • 模块化 - model 互相独立,又可以互相引用
  • 自动 loading 处理 - 异步方法不用再自己处理 showLoading hideLoading 等等
  • 按需触发重新渲染 - 在 Hooks 中传入 deps 参数即可,与 useEffect 的 deps 一样
// counter.js
import use from 'flooks';

const counter = {
  count: 0,
  add() {
    const { count } = use();
    use({ count: count + 1 });
  },
};

export default use(counter); // exports a React Hook¹, also a model getter²
// trigger.js
import use from 'flooks';
import counter from 'path/to/counter.js'; // import as `counter`, a model getter²

const trigger = {
  async addLater() {
    const { add } = counter();
    await new Promise((resolve = setTimeout(resolve, 1000)));
    add();
  },
};

export default use(trigger);
// Demo.jsx
import useCounter from 'path/to/counter.js'; // import as `useCounter`, a React Hook¹
import useTrigger from 'path/to/trigger.js';

function Demo() {
  const { count, add } = useCounter(['count']); // `deps` for rerender control
  const { addLater } = useTrigger(); // `addLater.loading` auto loading state 
  return (
    <>
      <p>{count}</p>
      <button onClick={add}>+</button>
      <button onClick={addLater}>+ ⌛{addLater.loading && '...'}</button>
    </>
  );
}

欲了解更多,请查看下面的 GitHub 链接。

8. Introducing flooks 2.0

🍸 flooks 2.0

也许是最简单的 React Hooks 状态管理器,只有一个 API use。

GitHub: github.com/nanxiaobei/…

在线示例: codesandbox.io/s/flooks-gq…

快来试试吧。

9. 其它可能呢?

其实我还实现了一种只有 useModel 和 setModel(与 1.0 的不是一回事)的 API:

// model
import { setModel } from 'flooks';
import another from 'path/to/another.js';

const counter = {
  count: 0,
  add() {
    const { count } = counter;
    const { num } = another;
    setModel({ count: count + 1 });
  },
};

export default counter;
// component
import { useModel } from 'flooks';
import counter from 'path/to/counter.js';

const App = () => {
  const { count } = useModel(counter, ['count']);
}

这个实现完全符合 js 本身对象的结构与引用方式,同时实现了对其中数据的动态更新。

但也正因如此,看起来像静态引入,数据又是动态,所以实现起来全是 mutable,不太函数式,看起来不够酷,于是被我暂存起来了。

10. 完了

flooks 2.0 正式发布,欢迎尝试~

github.com/nanxiaobei/…