(但是,只在封闭系统生效!)
早些时候,我对 hooks 下使用 useReducer 或者一众状态管理库是非常方案的,带上在结合项目仔细思考之下,发现了这一方案的优点,或许结合使用才是最优解
对 useReducer 的产生误解的原因也很简单:
- 类型支持非常差(js/ts 没有很好的逆变类型支持,没有模式识别,对比 reasonml 下的 useReducer 可知)
- 无模式识别(同上)
- 无法在 useReducer 内部,利用第三方 hooks 生态(即封装状态逻辑视图的全功能第三方库)
很简单一个例子,const {data,error} = useSwr(key,fetcher)
,这段代码将请求过程,全部转化为数据驱动的 data,error 状态,同时响应式支持了请求,延迟,屏幕聚焦,轮训,防抖等等功能
开发过程中使用这样的 第三方 api 能够得到事半功倍的效果
但是,如果你在这样的逻辑上,使用了 useReducer ,那么,这个叫做 useSwr 的 api,你无法再使用了,因为 reducer 内部代码(脱离了 react 调度)
当然,这里说的是 useReducer 的劣势,redux,mobx 等工具不在讨论范围内,,因为缺乏 module 功能,没有初始化的控制能力(即状态管理功能无法随着组件的初始化而初始化,迭代,异步渲染中失去了可编程性),导致大部分场景下已经缺乏使用价值(除了跨平台,又想要省略 dsl 编译过程,不过这种需求非常罕见,不是正道,比如 rn 和 flutter 方案,flutter 原理上更胜一筹)
不过,如果缺乏专业产品经理或者业务专家,没有设计,叠加新版本相关生态不够完善的情况下(比如 antd 的 useForm 不是无视图依赖的逻辑api,导致
@testing-library/react-hooks
无法使用),redux 等工具的调试方式可能更适合不稳定项目(生态完善的情况下,依然优先 TDD,即便缺乏顶层设计,因为逻辑本身是抽象的,靠打印进行的调试隔着一层 dom,一层 vm,绝对不是好的选择,直接调试逻辑才是标准流程)
这么说来,useReducer 在 hooks 环境下,就毫无作用可言了么?
前文有提过,至少 react 所举的例子,并不能说明 useReducer 的意义:
仅仅是因为 useCallback 有变化?就需要用 useReducer dispatch?
const [todos,dispatch] = useReducer(todosReducer)
return <TodosState value={todos}>
<DeepTreeWithState/>
<TodosDispatch.Provider value={dispatch}>
<DeepTreeWithDispatch />
</TodosDispatch.Provider>
</TodosState>
复制代码
甚至标准做法还应该做的这么丑陋?
拜托,这个实在误人子弟了,控制 view 永远靠 useMemo,调度逻辑也是业务逻辑的一部分,哪能就这么回避掉的?
直接返回未拆分的 jsx 本身的含义就是 —— 视图跟随所有 state,props,context 变化,逻辑即如此,反而 dispatch 虽不说不伦不类,也有点反直觉
useReducer 该用么?现在告诉大家,某些情况下的确该用,但是不是 react 文档提到的这种情况(至少绝不会像他说的那么简单)
什么情况下需要用到 useReducer ?
封闭系统下的 控熵
熵:泛指某些物质系统状态的一种量度,某些物质系统状态可能出现的程度
可以近似理解为,同一时刻,物体可能状态的数量,其中既包含状态的 数量,也包含状态的 可能性
还记得前文总结的么?React 虽然在 hooks 这一次更新,做了很大的函数式改进,但是数据驱动原则始终未变
毕竟直接由具象转化为抽象编程,跨度太大,前端又多是处在视图开发层次,状态或许比事件更加重要?
然而函数式是不应该有太多状态,无共享状态甚至是无状态的,因为函数本身是对变化的抽象,用状态模拟概念编程,补足函数式顶层设计短板的做法,其实是有很大牺牲的(比如缺乏概念编程的自解释性,类型无协变,即类型无法自顶向下等)
但是,现实是,前端必须两者兼得,必须尊重状态,因为前端开发本身就是中介,本身就是视图和一致性数据的中间件,状态绕不过去
同样,即便是事件驱动(zone)著称的 angular,也没有像 cyclejs 那样无状态流一撸到底,cyclejs 的不温不火也能反过来证明这一点
那么, 状态,以及背后代表的意义,就耐人寻味了
因为
状态就是熵
下面这段表述我相信对 react 程序员来说,都是入门经典:
没错!曼妥思糖和可乐!
曼妥思碰到了可乐,双方产生剧烈反应,一方的不确定性和状态数量会直接灌入另一方中,这就是个剧烈的熵增反应
左侧是你的应用中的状态,右侧是你收到的数据请求的状态,一旦两者想触碰,会发生什么?
注意,将数据和数据变化的可能性,都要作为状态统一看待,熵本身就是状态可能性集合,因此,除了请求返回数据,还有请求返回时间,错误,多个请求的关系,race 还是更加复杂的调度?
一边,是确定调度,确定结构的 react 应用,另一边,是不确定数据,不确定调度的异步
将两者看做一个系统,怎么解决这个问题?
没错!麦克斯韦妖!
假设出现一个有选择能力的主体 —— 麦克斯韦妖,它选择性地将结果放入另一个结构(react 应用/store)中,就可以解决这个问题
一边是确定性(低熵)的 state,通过这个 麦克斯韦妖(dispatch)选择 (reducer),很好地保证了 store
的低熵环境
const [state,dispatch] = useReducer((state,action)=>{
if(action.name === 'xxx'){
// 麦克斯韦妖/reducer 正在进行选择
}
// 麦克斯韦妖/reducer 正在放入另一侧的 state
return {...state}
},{})
复制代码
因此,前文提到的某个情景 —— 即应对非合成事件 的时候,callback 调用烧脑的问题(区分内外,区分调度源),useReducer 是个非常好的解决方案
这个时候,dispatch 的不变性,才有了很好的使用场景:
const ws = useRef()
const [state,dispatch] = useReducer((state,action)=>{
if(action.payload === 'init'){
ws.current = new WebSocker('...')
}
if(action.payload === 'read'){
ws.current.send('read')
}
// ...
},{data:'',error:null})
// 这样,应用本身的结构,可以很容易(effect,合成事件)与另一部分(非合成事件)进行交流
useEffect(()=>{
if(ready){
dispatch('init')
}
},[ready])
return <button onClick={()=>dispatch('init')}>init</button>
复制代码
这样的话,不确定性会比纯粹使用 useCallback 低得多(因为依赖很难得到处理,至少很烧脑)
整个应用的熵得到了非常好的控制
useReducer,系统对接的神器
是的,当我们想要将一个封闭系统,与另一个系统对接,形成更大的封闭系统时,useReducer 就非常有用了,因为它控制住了不确定性
将 dispatch 想象成麦克斯韦妖,你就能很轻松实现多个系统之间的隔离,并且有可预知的结果
那么,除了异步以外,还有哪些地方可以用到呢?
跨模块通讯!
这部分以后可以继续分享,因为涉及一个笔者仍没有彻底弄懂的架构技术
那全用 useReducer 可以么?
本来是可以的,除非你不想要哪些全封装的第三方hooks api,以及不想要更简单明了的流
但是效率和质量是可以做权衡的,这部分看你怎么看,不过一旦你全使用 useReducer 进行开发,就是默认了,有 useReducer 处,就是个单独的模块
也就是说,要么全用 useReducer,要么只用 useReducer 做模块通讯(其他调度系统的通讯也可以看做模块通讯)