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'); }; 复制代码
虽然它号称最简单(好吧其实就我号称的),但似乎也没有那么简单,粗一看还是不容易一眼看明白。
当初为了简化使用,使用了 useModel()
传入字符串的方式,从而无需引入文件获取 model。但重新考虑后,这种方式在开发时,并没有引入文件科学:
① 引入文件时,可以很清晰的知道自己在干什么,需要引入哪个文件,
② 引入文件利用 IDE 可以快速跳到指定文件,
③ 精简是设计目标,但精简不应以牺牲明确为代价,“抽象”在开发时应让位于“清晰”;
setModel()
的 API 设计略显臃肿,虽然 setModel(name, model)
的调用方式足够清晰,但它就是没那么简洁,没那么好看,让人忍不住看两眼就烦;
重点来自于 model 中 actions
的使用方式,借鉴 rematch 使用函数传参引入功能,这个比 setModel()
的设计更让人崩溃,越看越丑,什么玩意?
model 中分为 state
和 actions
两部分,不够简洁,最重要的是不符合直觉。从直觉来说,不管是 useModel()
还是 model()
,理论上拿到的应该是对象第一层 state
和 actions
,而非第二层子数据;
model()
与 useModel()
的问题一样,也是字符串传参,经重新考虑,这种方式并没有直接引入文件直观便利。
总之,直觉很重要,要符合直觉。
actions
这种奇怪的东西)setModel()
、model()
这样猥琐的 API 设计;state
与 actions
,它们并不是必须,也并没有那么重要;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 特供状态管理器。
先忘掉 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.
下面是一个完整示例,展示了 flooks 2.0 的功能特点:
showLoading
hideLoading
等等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 链接。
🍸 flooks 2.0
也许是最简单的 React Hooks 状态管理器,只有一个 API use
。
GitHub: github.com/nanxiaobei/…
在线示例: codesandbox.io/s/flooks-gq…
快来试试吧。
其实我还实现了一种只有 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,不太函数式,看起来不够酷,于是被我暂存起来了。
flooks 2.0 正式发布,欢迎尝试~
github.com/nanxiaobei/…