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

·  阅读 1602

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 中分为 stateactions 两部分,不够简洁,最重要的是不符合直觉。从直觉来说,不管是 useModel() 还是 model(),理论上拿到的应该是对象第一层 stateactions,而非第二层子数据;

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

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

3. flooks 改进目标

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

4. 如何改进?

  1. 不使用字符串传参设计,改为直接引入文件;
  2. 改进 setModel()model() 这样猥琐的 API 设计;
  3. 干掉 stateactions,它们并不是必须,也并没有那么重要;
  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
  },
};
复制代码

是的,这样就够了,有数据,有方法,并不需要 stateactions

上面说了,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
复制代码

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

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

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

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

前面这些功能,最大的难点来自于如何区分 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 参数即可,与 useEffectdeps 一样
// 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. 其它可能呢?

其实我还实现了一种只有 useModelsetModel(与 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/…

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改