Re−frame 入門


以前、[Om はじめの一歩](https://speakerdeck.com/snufkon/om-hazimefalse-bu), [入門 Om Next](https://qiita.com/snufkon/items/b19a76a83adce3dc2887) といった React の ClojureScript ラッパーの記事を書きました。今回も、**TODOリスト**を題材に Re-frame での基本的な書き方を確認してみたいと思います。

## Re-frame とは

ClojureScript には React のラッパーがいくつかあります。前述の Om、Om Next や Reagent、 最近だと Rum なども見かけます。Re-frame はこの React ラッパーの１つである Reagent のフレームワークという位置づけで、Reagent ベースで SPA を作る際の共通となる枠組みを提供します。この提供される枠組みは Redux により提供される枠組みに近く、JavaScript における React+Redux での開発、Swift における ReSwift を使った開発など、他の言語で Redux ライクな開発を経験している人には扱いやすく、導入の敷居が低いフレームワークと言えます。

## TODO リストを作る

クライアントサイドで完結するTODOリストを作成し、Re-frame の使い方を確認していきたいと思います。

仕様は[入門 Om Next](https://qiita.com/snufkon/items/b19a76a83adce3dc2887)の時と同じで、以下となります。

### 仕様

- TODO を表示することができる
- TODO を追加することができる
- TODO を完了/取消しすることができる
- TODO を削除することができる

### 動作確認方法

最終的に完成したコードは [github](https://github.com/snufkon/re-frame-todolist) においてあり、以下の方法で動作確認をすることができます。

```
$ git clone https://github.com/snufkon/re-frame-todolist
$ cd re-frame-todolist
$ lein figwheel dev
```

コンパイル完了後、 http://localhost:3449 にアクセス。

また、各ステップの実装完了後の状態で tag を作成してあるので、必要があれば tag をチェックアウトして動作確認してください。

```
$ git checkout -b step0 step0
```


### Step0: プロジェクト作成

re-frame の[テンプレート](https://github.com/Day8/re-frame-template)を使いプロジェクトの雛形を作成します。

```
$ lein new re-frame re-frame-todolist
```

作成されるプロジェクトのディレクトリ構成は以下のようになります。

```
$ tree re-frame-todolist

re-frame-todolist
├── README.md
├── project.clj
├── resources
│   └── public
│       └── index.html
└── src
    ├── clj
    │   └── re_frame_todolist
    │       └── core.clj
    └── cljs
        └── re_frame_todolist
            ├── config.cljs
            ├── core.cljs    <-- 変更対象
            ├── db.cljs      <-- 変更対象
            ├── events.cljs  <-- 変更対象
            ├── subs.cljs    <-- 変更対象
            └── views.cljs   <-- 変更対象
```

特に変わったところはなく、一般的な Clojure/ClojureScript のプロジェクト構成です。今回作成するTODOリストの実装は、クライアントサイドだけで完結するため、clj への変更は行わず、上記で示した5つの cljs ファイルのみに変更を加えていきます。


#### 動作確認

TODOリストを作成する前に、テンプレートで作成されたプロジェクトの動作確認しておきます。

```
$ lein clean
$ rlwrap lein figwheel dev
```

特に問題がなければ http://localhost:3449 へのアクセスで、以下のように"Hello from re-frame"といったメッセージが表示されると思います。

![step0.png](https://qiita-image-store.s3.amazonaws.com/0/20642/2d235d58-d275-bf89-8ada-a0aa14739525.png "step0.png")


#### 各ファイルの役割

編集対象の各ファイルの役割は、主に以下のようになります。

- core.cljs: アプリケーションのエントリポイントを提供。初期化に関するコードを記述
- db.cljs: アプリケーションの初期状態に関するコードを記述
- subs.cljs: View からの問い合わせに答え、アプリケーション状態を加工した結果を返すためのコードを記述
- view.cljs: Reagentコンポーネントを使い View を作成するコードを記述
- events.cljs: View からディスパッチされたイベントを処理するハンドラーのコードを記述


#### コード確認

"Hello from re-frame" がどのように表示されるかを追いながら、初期状態のコードを確認します。

まず、エントリポイントとなる `core.cljs` から見ていきましょう。

**core.cljs**

```clj:core.cljs
(ns re-frame-todolist.core
  (:require [reagent.core :as reagent]
            [re-frame.core :as re-frame]
            [re-frame-todolist.events :as events]
            [re-frame-todolist.views :as views]
            [re-frame-todolist.config :as config]))


(defn dev-setup []
  (when config/debug?
    (enable-console-print!)
    (println "dev mode")))

(defn mount-root []
  (re-frame/clear-subscription-cache!)
  (reagent/render [views/main-panel]
                  (.getElementById js/document "app")))

(defn ^:export init []
  (re-frame/dispatch-sync [::events/initialize-db])
  (dev-setup)
  (mount-root))
```  

`init` 関数が `index.html` から呼ばれるアプリーケーションのエントリポイントとなります。`init` 関数ではまず、`dispatch-sync` 関数により、`::initialize-db` イベントを発行し、アプリケーション状態の初期化を行っています。実際の初期化処理は、`events.cljs` に記載した `::initialize-db` イベントのハンドラーによって行われます。アプリケーション状態の初期化完了後は、`render` 関数でアプリケーションのルートとなる `main-panel` コンポーネントの描画を行っています。

**events.cljs**

```clj:events.cljs
(ns re-frame-todolist.events
  (:require [re-frame.core :as re-frame]
            [re-frame-todolist.db :as db]))

(re-frame/reg-event-db
 ::initialize-db
 (fn [_ _]    
   db/default-db))
```

`events.cljs` では `reg-event-db` 関数を使いイベントハンドラーを登録します。 `::initialize-db` がイベントを表すIDで、`(fn [_ _] db/default-db)` の部分がハンドラです。ハンドラは引数として、第一引数として、現在のアプリケーション状態、第二引数としてイベントとパラメータのベクターを受け取ります。また、ハンドラが返す値が次のアプリケーション状態になります。`::initialize-db` では現状のアプリケーション状態に関わらず、`default-db` を返すことで初期化を行っています。アプリケーション状態の変更はこの `event.cljs` 内のみで実施するようにします。


**db.cljs**

```clj:db.cljs
(ns re-frame-todolist.db)

(def default-db
  {:name "re-frame"})
```

`events.cljs` での初期化時に利用する `default-db` が定義されています。現状は画面に表示されている "Hello from re-frame" の "re-frame" 部分を保持する `:name` キーが登録されています。


**view.cljs**

```clj:view.cljs
(ns re-frame-todolist.views
  (:require [re-frame.core :as re-frame]
            [re-frame-todolist.subs :as subs]))

(defn main-panel []
  (let [name (re-frame/subscribe [::subs/name])]
    [:div "Hello from " @name]))
```

`view.cljs` には `core.cljs` からルートとして描画される `main-panel` コンポーネントが定義されています。`main-panel` は Reagent のコンポーネントで、通常の ClojureScript の関数として定義し、Hiccup 形式で描画する HTML を返すようにします。画面に表示されている "Hello from re-frame" の文字列のうち "re-frame" の部分はアプリケーション状態を取得し表示してます。アプリケーション状態の取得は、直接 `default-db` を参照するのではなく、`subscribe` 関数を使い re-frame が管理しているアプリケーション状態を参照するようにします。`subscribe` している値が変更されるとコンポーネントの再描画が行われます。


**subs.cljs**

```clj:subs.cljs
(ns re-frame-todolist.subs
  (:require [re-frame.core :as re-frame]))

(re-frame/reg-sub
 ::name
 (fn [db]
   (:name db)))
```

`subs.cljs` では `reg-sub` 関数を使い view からの問い合わせに応じて値を返す処理(query関数)を登録します。re-frame で管理しているアプリケーション状態の全体を返すのではなく、コンポーネントで利用する単位、view に適した情報に加工し返すようにします。



### Step1: TODO リスト表示

では、TODOリストの実装に入っていきたいと思います。

![step1.png](https://qiita-image-store.s3.amazonaws.com/0/20642/83e7b8df-5764-0dc8-2064-22287484162c.png "step1.png")


最初に、ハードコーディングされた TODOリストを表示します。変更が発生する部分のコードのみ記載および説明していきます。

```clj:db.cljs
(def default-db
  {:todos {1 {:id 1 :title "豚肉を買ってくる"}
           2 {:id 2 :title "たまねぎを買ってくる"}
           3 {:id 3 :title "にんじんを買ってくる"}
           4 {:id 4 :title "じゃがいもを買ってくる"}
           5 {:id 5 :title "カレーを作る"}}})
```

まず、初期データに表示するTODOリストを設定します。TODO を取得、削除する際に id で該当の TODO を取ってくる際に都合がよいため、データ構造はマップにしておきます。また、`:name` キーは不要のため削除します。


```clj:subs.cljs
(re-frame/reg-sub
 ::todos
 (fn [db]
   (-> db :todos vals)))
```

view からの問い合わせに応じて、TODOリストを返す処理を登録します。view で利用する際に不要なマップのキー情報は削除してTODOリストを返すようします。`::name` に関する `reg-sub` 関数は不要のため削除します。


```clj:view.cljs
(defn todo-item
  [todo]
  [:li (:title todo)])

(defn todo-list
  []
  (let [todos @(re-frame/subscribe [::subs/todos])]
    [:ul
     (for [todo todos]
       ^{:key (:id todo)}
       [todo-item todo])]))
```

TODOリストを表示するための `todo-list` コンポーネントを追加します。`subscribe` 関数で `subs.cljs` に登録した `::todos` を購読しているため、todos に変更があると更新された値が取得され、コンポーネントが再描画されます。１つ１つの TODO は `todo-item` コンポーネントで表示するようにします。`main-panel` は不要なため削除します。


```core.cljs
(defn mount-root []
  (re-frame/clear-subscription-cache!)
  (reagent/render [views/todo-list]
                  (.getElementById js/document "app")))
```  

`main-panel` ではなく TODOリストを表示するために作成した `todo-list` コンポーネントをルートとして表示するように変更します。

### Step2: TODO 追加

TODOリストの上部に入力欄を配置し、テキスト入力後、Enterキーを押すことでTODO項目を追加できるようにします。

![step2.png](https://qiita-image-store.s3.amazonaws.com/0/20642/3ffbc09d-1050-35b7-105d-6816197788b7.png "step2.png")



```clj:view.cljs
(defn todo-input
  []
  (let [val (reagent/atom "")]
    (fn []
      [:input {:type        "text"
               :value       @val
               :class       "new-todo"
               :placeholder "What needs to be done?"
               :on-change   #(reset! val (-> % .-target .-value))
               :on-key-down #(when (= (.-which %) 13)
                               (let [title (-> @val cstr/trim)]
                                 (when (seq title)
                                   (re-frame/dispatch [::events/add-todo title]))
                                 (reset! val "")))}])))

(defn todo-list
  []
  (let [todos @(re-frame/subscribe [::subs/todos])]
    [:div
     [todo-input] ;; <-- 追加
     [:ul
      (for [todo todos]
        ^{:key (:id todo)}
        [todo-item todo])]]))
```

`todo-list` コンポーネントに新しいTODOの名前を入力する `todo-input` コンポーネントを追加します。`todo-input` コンポーネントは `todo-list` コンポーネントのように Hiccup 形式をそのまま返すのではなく、関数を返しています。これは、入力途中のテキストをコンポーネントローカルで保持させるための設定 `[val (reagent/atom "")]`を記述するためです。いつくかある Reagent コンポーネントを作成する書き方の１つで、コンポーネント単位での初期化、ローカル状態の作成などを行う際に利用します。詳しくは公式のドキュメントを確認してみてください。[^1]

テキストを入力し、エンターキー押下で現状のテキストをパラメータとしてTODO追加のイベント`[::events/add-todo title]`を発行します。実際の追加処理は下記の `events.cljs` で行います。


```clj:events.cljs
(defn- allocate-next-id
  [todos]
  (-> todos
      keys
      last
      ((fnil inc 0))))

(re-frame/reg-event-db
 ::add-todo
 (fn [db [event title]]
   (let [id (allocate-next-id (:todos db))]
     (update db :todos #(assoc % id {:id id :title title})))))
```

view からの `::add-todo` イベントを受け、新しいTODO項目を追加するため処理を `reg-event-db` 関数で登録します。 受け取った title を持つ todo を作成し、現状のアプリケーション状態に update をかける形で TODO を追加します。TODOが追加されると、`::subs/todos` を `subscribe` している `todo-list` が再描画され、TODOの追加が画面のリストに反映されます。



### Step3: TODO 完了/取消し

次にチェックボックスのON/OFFで、TODO項目の完了/取消しを表現できるようにします。

![step3.png](https://qiita-image-store.s3.amazonaws.com/0/20642/77ecbfbf-3a27-c31b-149c-732bd38b7e7a.png "step3.png")


```clj:db.cljs
(def default-db
  {:todos {1 {:id 1 :title "豚肉を買ってくる" :completed true}
           2 {:id 2 :title "たまねぎを買ってくる" :completed true}
           3 {:id 3 :title "にんじんを買ってくる" :completed false}
           4 {:id 4 :title "じゃがいもを買ってくる" :completed false}
           5 {:id 5 :title "カレーを作る" :completed false}}})
```

完了状態を管理するための`:completed` キーを追加しておきます。

```clj:view.cljs
(defn todo-item
  [{:keys [id completed] :as todo}]
  [:li  
   [:input {:type "checkbox"
            :class "toggle"
            :checked (and completed "checked")
            :on-change #(re-frame/dispatch [::events/toggle id])}]
   [:span {:class (when completed "completed")}
    (:title todo)]])
```

各TODOにチェックボックスを追加します。チェックボックスのOn/Off で `::events/toggle` イベントを発行し、実際の処理は `events.cljs` に記述した対応するイベントハンドラーに依頼します。


```clj:events.cljs
(re-frame/reg-event-db
 ::add-todo
 (fn [db [event title]]
   (let [id (allocate-next-id (:todos db))
         new-todo {:id id :title title :completed false}] ;; completed を追加
     (update db :todos #(assoc % id new-todo)))))

(re-frame/reg-event-db
 ::toggle
 (fn [db [event id]]
   (update-in db [:todos id :completed] not)))
```

`view.cljs` で dispatch の際に渡された id を使い、対応するTODOの完了状態をトグルします。
また、`::add-todo` で新規TODOを追加する際には、completed を false に設定するように追記します。



### Step4: TODO 削除

最後に、TODOを削除できるようにします。

![step4.png](https://qiita-image-store.s3.amazonaws.com/0/20642/82b8fdd6-7c70-e7bc-772d-995a145e23d4.png "step4.png")

```clj:view.cljs
(defn todo-item
  [{:keys [id completed] :as todo}]
  [:li
   [:input {:type "checkbox"
            :class "toggle"
            :checked (and completed "checked")
            :on-change #(re-frame/dispatch [::events/toggle id])}]
   [:span {:class (when completed "completed")}
    (:title todo)]

   ;; 追加
   [:span {:class "delete"                     
           :on-click #(re-frame/dispatch [::events/delete id])}
    "[x]"]])
```        

削除イベントを発行するためのボタンを追加します。ボタン押下で対象となるTODO項目の id をパラメータとしたイベント`[::events/delete id]`を発行し、実際の削除処理は下記の `events.cljs` で行います。

```clj:events.cljs
(re-frame/reg-event-db
 ::delete
 (fn [db [event id]]
   (update db :todos dissoc id)))
```   

`view.cljs` で dispatch の際に渡された id を使い、対応するTODOを削除します。


## おわりに

TODOリストを作成することで、Re-frame でのコードの書き方を確認しました。Re-frame の提供する枠組みに従うことで、アプリケーション状態の管理とViewの構築を切り分けたコーディングを行うことができアプリケーションの規模が大きくなった際にも見通しの良いコードが書けそうです。とは言え、フレームワークの提供する制約は弱く、単純に導入するだけではなく view = f(state) を意識して実装していくことが大切だと思います。

今回の記事で Re-frame に興味を持った方は、次の学習として公式にある [TodoMVC のサンプル](https://github.com/Day8/re-frame/tree/master/examples/todomvc)を確認してみると良いと思います。今回触れなかった機能(interceptorとか)やローカルストレージとの連携、spec による状態のバリデーションなどより実践的な使い方を学ぶことができます。

今年は、Om Next のバージョンが beta となりましたが、あまり開発もアクティブではなく、現時点では Re-frame & Reagent がファーストチョイスかなと思います。[^2]

# 参考資料

- [re-frame](https://github.com/Day8/re-frame)
- [The Re-frame Building Blocks Guide](https://purelyfunctional.tv/guide/re-frame-building-blocks/)


[^1]: [Creating-Reagent-Component](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components)

[^2]: Om Next or Re-frame を検討している方には[こちらの記事](https://purelyfunctional.tv/article/why-re-frame-instead-of-om-next/)が参考になると思います。