ClojureScriptの状態管理にIntegrantを使う
フロントエンド開発においてリローダブルでないとはどういうことかというと、例えば以下のような状況が考えられる。 簡単な例として、ブラウザの現在のサイズを表示するだけのアプリを実装してみる。 code:core.cljs
(defonce system (atom nil))
ここにIntegrantで起動したシステムを格納することにする。defonceを使うことで自動的にリロードされたとしても、systemは再束縛されない。 画面に表示する部分はRumを使って、以下のように実装した。 code:core.cljs
(rum/defc display-window-size < rum/reactive
[:div
dbというatomを受け取って、ブラウザのサイズが変わってもちゃんと表示が変わるようになっている。 ウィンドウのサイズが変更されるのは、リサイズイベントを取ればよいので、これは以下のように書くことができる。
code:core.cljs
(defn current-window-size []
(let [width (.-innerWidth js/window)
height (.-innerHeight js/window)]
{:width width :height height}))
(defmethod ig/init-key :demo/event
db
(reset! db (current-window-size)))]
(events/listen js/window "resize" handler)
handler))
(defmethod ig/halt-key! :demo/event
handler
(events/listen js/window "resize" handler))
イベントリスナーに登録するイベントハンドラーは、削除するさいにも必要なわけだけれど、普通に書こうとすると少しばかり面倒だったりする(特に今回のdbみたいに外部のものに依存している場合は)。 しかし、このようにIntegrantを使えば、システムの中にイベントハンドラーを保持しておくことができ、システムを停止するときに利用することができる。 RumのコンポーネントをマウントするためのDOMも必要となる(若干こじつけだけれど)。それは、次のように書くことができる。 code:core.cljs
(defmethod ig/init-key :demo/dom
id
(dom/appendChild (.-body js/document)
dom)
dom))
(defmethod ig/halt-key! :demo/dom
elm
(dom/removeNode elm))
見て分かる通り、イベントハンドラーと同様に作成したDOMをシステムで保持することによって、停止するときにそのDOMを直接削除することができる。これはいちいちquerySelectorなどを利用することがないという点でも優れている。 残りはアプリケーションで必要になるデータを保持するデータベースと、Rumのコンポーネントをマウントするための処理だ。簡単なのでさくっと紹介してしまうことにする。 code:core.cljs
(defmethod ig/init-key :demo/db
_
(atom (current-window-size)))
(defmethod ig/halt-key! :demo/db
db
(reset! db nil))
(defmethod ig/init-key :demo/app
(rum/mount (display-window-size db) elm))
dbは特に何にも依存していないので、現在のウィンドウサイズを保持したatomを作成するだけでよい。システムを停止させるときの処理は必要ないが、なんとなく気分で書いた。また、Rumのコンポーネントを初期化するための、コンポーネント(言葉が紛らわしい)はdbとマウント先のDOMに依存しているため、初期化するさいに受け取りRumのコンポーネントをマウントする。 最後に、システムの依存関係を設定として記述してシステムを起動/停止する関数を用意すればよい。 code:core.cljs
(def system-conf
{:demo/dom "myapp"
:demo/event (ig/ref :demo/db)
:demo/db nil
:demo/app {:db (ig/ref :demo/db)
:elm (ig/ref :demo/dom)}})
(defn start []
(reset! system (ig/init system-conf)))
(defn stop []
(ig/halt! @system)
(reset! system nil))
ClojureScriptの場合は、ig/read-stringを利用することができない(できたとしてブラウザは何処からリソースファイルを貰えばいいんだ…)ので、直接ファイルに設定を書いてしまえばよい。 Integrantを使うメリットのひとつに初期化するときの依存関係を決定できるというのがある。DOMがないとマウントできないとか、dbを初期化した後でないとイベントハンドラーの内部で利用できないといった問題も一緒に解決できる。 今回作ったサンプルは以下に置いておくので、興味があれば適当に参照してほしい。