Clojure China

关于 Respo Hooks 功能更新的简介

#1

近期对 Respo 的状态管理方案进行了一次更新,
具体的代码可以看 https://github.com/Respo/respo/pull/23

起因

Respo 的 States 方案为了方便热替换时保存状态, 做了一些限制,

  • 组件挂载的时候以及渲染过程当中不能 dispatch 事件,
  • states 以树形的方式存储, 需要手动调用 cursor-> 在组件之间传递分支,
  • 修改状态使用的路径, 也就是 cursor, 跟组件对应, 方便简写.

React Hooks 出来的时候, 我当然意识到了 Respo 功能的不足,
之前比较久了, 我为 Respo 加上了基础的 Effects, 可以加一些 DOM 操作,
React Hooks 可以把部分的组件状态抽到插件里去, Respo 不行.
Respo 的 cursor 是跟组件绑定的, 插件写法无法传递 cursor.

另一方面, 我在 Phlox 项目当中由于需要, 设计了个简单的 states tree 方案,
这个方案里 cursor 是需要用户手工传递的, 比较啰嗦, 但是勉强够用.
后来我仔细想想, 这个方案对于 Respo 来说, 也是够用的.
虽然写起来会啰嗦, 但是用户可以手动传递 cursor, 也就意味着可以拆成插件复用.

代码示例

文档上写得也不详细, 这边稍微再描述一下.
首先 Respo 的状态树, 大致上是这样一个结构,
其中 :data 专门用于存储节点的数据, :cursor 作为保留字段,
这就是一个树形节奏, 跟组件直接对应, 但是大致跟状态分支对应上:

{
  :cursor []
  :files {
    :ns {
      :add {
        :data {:show? false, :failure nil}
        :modal {
          :data {:text nil, :failure nil}
        }
      }
      "app.rude" {
        ":rmapp.rude" {
          :data {:show? false}
        }
      }
    }
    "app.rude" {
      :add {
        :data {:show? false, :failure nil}
        :modal {
          :data {:text nil, :failure nil}
        }
      }
    }
  }
}

需要更新状态时, 需要一个 cursor, 可以是 [:file :ns "app.rude" ":rmapp.rude"],
后面还有个 :data, 有了 cursor, 就能定位到数据做更新了.
后面大致想象, 组件用的就是一个分支的数据, 而 cursor 对应分支的路径.
具体在代码当中比较啰嗦, 不过比较容易可以维护两者的对应关系.
最终得到类似组件局部状态的一个效果.

为了方便书写, 我增加了一个 >> 函数, 把 states 和 cursor 一起传递.
经过杂七杂八的抽象以后, 最终得到这样效果的代码:


这中间是省略了好多的过程, 也不打算很详细描述了, 具体要看文档(还没补好).

相应地, 更新状态的部分我加了 update-states 函数, 作为一个简写.
状态更新作为 dispatch action 的一种特殊情况, 跟 dispatch 一起被处理.
如果没看之前, 我明确一下, Respo 当中 states 是跟 store 一起存储在全局的.
能分支读取, 能维护 cursor 做更新, store 当中能响应, 整个流程串起来了.

延伸的影响

增加这块功能主要的目标, 跟 React Hooks 类似, 为了逻辑的复用,
Respo 的组件跟 React 类似, 不允许从外部操作状态,
这就意味着我封装出来的 Modal 组件显得比较奇怪了, 或者说死板,
要么我外边维护一个 visible 状态, 传进去, 并且加上 on-change 做切换,
要么我把触发打开关闭的部分也放进组件里边, 这样使用起来就有点僵化了.

而 Hooks 形态的写法, 开始允许状态被抽取到一个独立的函数当中,
比如我调整过的 prompt 用法, 就可以抽出的一个插件当中,

(defn use-prompt [states options]
  (let [cursor (:cursor states), state (or (:data states) {:show? false, :failure nil})]
    {:ui (comp-prompt-modal
          (>> states :modal)
          options
          (:show? state)
          (fn [text d!]
            (if (some? @*next-prompt-task) (@*next-prompt-task text))
            (reset! *next-prompt-task nil)
            (d! cursor (assoc state :show? false)))
          (fn [d!] (d! cursor (assoc state :show? false)) (reset! *next-prompt-task nil))),
     :show (fn [d! next-task]
       (reset! *next-prompt-task next-task)
       (d! cursor (assoc state :show? true)))}))

插件暴露 uishow 方法两部分, ui 用于渲染, show 方法用户更新状态.
这样, 以往代码当中相当多的弹层的逻辑就可以抽出做复用了.

后续代码会继续更新. 目前也认识到 Respo Hooks 相比 React Hooks 比较局限比较多,
特别是 Effects 那块, React 做得比较强大了, Respo 这方面功能很弱.
希望目前来说这个功能够用, 这样我能对 Calcit Editor 遗留的代码做一些整理.