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 有什么问题
-
当初为了简化使用,使用了
useModel()
传入字符串的方式,从而无需引入文件获取 model。但重新考虑后,这种方式在开发时,并没有引入文件科学:① 引入文件时,可以很清晰的知道自己在干什么,需要引入哪个文件,
② 引入文件利用 IDE 可以快速跳到指定文件,
③ 精简是设计目标,但精简不应以牺牲明确为代价,“抽象”在开发时应让位于“清晰”;
-
setModel()
的 API 设计略显臃肿,虽然setModel(name, model)
的调用方式足够清晰,但它就是没那么简洁,没那么好看,让人忍不住看两眼就烦; -
重点来自于 model 中
actions
的使用方式,借鉴 rematch 使用函数传参引入功能,这个比setModel()
的设计更让人崩溃,越看越丑,什么玩意? -
model 中分为
state
和actions
两部分,不够简洁,最重要的是不符合直觉。从直觉来说,不管是useModel()
还是model()
,理论上拿到的应该是对象第一层state
和actions
,而非第二层子数据; -
model()
与useModel()
的问题一样,也是字符串传参,经重新考虑,这种方式并没有直接引入文件直观便利。
总之,直觉很重要,要符合直觉。
3. flooks 改进目标
- 简洁(不能为了简洁牺牲易用,但简洁当然是重要的)
- 清晰(代码一眼看过去,不会一团乱,不会出现
actions
这种奇怪的东西) - 易用(以便于开发为目标,虽有时与简洁冲突,但它们也可以因天下大同的共同愿景而目标一致)
4. 如何改进?
- 不使用字符串传参设计,改为直接引入文件;
- 改进
setModel()
、model()
这样猥琐的 API 设计; - 干掉
state
与actions
,它们并不是必须,也并没有那么重要; - 为世界上最简单的 React Hooks 状态管理器而再坚持一下。
5. flooks 1.0 API 分析
setModel()
- initializeruseModel()
- React Hooksmodel()
- 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 正式发布,欢迎尝试~