ClojureScriptでRedux(と時々Rum)
基本方針
action は core.async で非同期に受け付ける
reducer はマルチメソッドとして実装する
dispatcher の役割をマルチメソッドの呼び出し結果で状態変更することのみにすることができる
状態を受け取って状態を返すだけのマルチメソッドなのでテストしやすい
新しいアクションへの対応も楽
更新
2018-06-13
transform-state のデフォルトの挙動が nil を返してしまっていたので、状態をそのまま返すよう修正
超ざっくりまとめ
code:core.cljs
(ns foo.core
(:require cljs.core.async :as async)
(:require-macros [cljs.core.async.macros :refer go-loop]))
(defonce app-state (atom {:text "hello"}))
(defonce event-bus (async/chan))
;; Reducer
;; 状態とアクションを受け取って新しい状態を返すマルチメソッド
(defmulti transform-state (fn state action (first action)))
(defmethod transform-state :default state _ state)
(defmethod transform-state :set-text
[state new-text]
(assoc state :text new-text))
;; アクションの待受
(go-loop []
(when-let action (async/<! event-bus)
(swap! app-state transform-state action)
(recur)))
;; イベントの発行
;; (async/put! event-bus :set-text "world")
Integrant, Rum を使ったもう少しまともな例
ボタンを押しただけ増えていくだけの例
lein new figwheel foo で作ったテンプレートを前提
code:core.cljs
(ns foo.core
(:require cljs.core.async :as async
integrant.core :as ig
rum.core :as rum)
(:require-macros [cljs.core.async.macros :refer go-loop]))
(defonce app (atom nil))
(def ^:private initial-state
{:num 1})
;; Component
;; ==================================================
(rum/defc num-block < rum/reactive
state event-bus
(let [{:keys num} (rum/react state)]
[:div (str "num = " num)
[:button {:on-click #(async/put! event-bus :inc-num)} "inc"]]))
;; Reducer
;; ==================================================
(defmulti transform-state (fn state action (first action)))
(defmethod transform-state :default state _ state)
(defmethod transform-state :inc-num
state _
(update state :num inc))
;; System
;; ==================================================
(defmethod ig/init-key :app/state
_
(atom initial-state))
(defmethod ig/halt-key! :app/state
state
(reset! state nil))
(defmethod ig/init-key :app.event/bus
_
(async/chan))
(defmethod ig/halt-key! :app.event/bus
ch
(async/close! ch))
(defmethod ig/init-key :app.event/dispatcher
{:keys state event-bus}
(go-loop []
(when-let action (async/<! event-bus)
(swap! state transform-state action)
(recur))))
(defmethod ig/init-key :app/init
{:keys state event-bus}
(js/console.log "start app")
(let node (js/document.querySelector "#app")
(rum/mount (num-block state event-bus) node)
node))
(defmethod ig/halt-key! :app/init
node
(rum/unmount node))
(def app-config
{:app/state {}
:app.event/bus {}
:app.event/dispatcher
{:state (ig/ref :app/state)
:event-bus (ig/ref :app.event/bus)}
:app/init
{:state (ig/ref :app/state)
:event-bus (ig/ref :app.event/bus)}})
(defn start-app! []
(when-not @app
(reset! app (ig/init app-config))))
(defn stop-app! []
(when @app
(ig/halt! @app)
(reset! app nil)))
(defn on-js-reload []
(stop-app!)
(start-app!))
(start-app!)
参考
https://slonoed.net/redux-in-closurescript-with-rum
http://tonsky.me/blog/datascript-chat/
こちらは Flux
#ClojureScript #Redux #Flux #Rum #Integrant
#2018-06-12 #2018-06 #2018
#記事