Help us understand the problem. What is going on with this article?

Re−frame 入門

以前、Om はじめの一歩, 入門 Om Next といった React の ClojureScript ラッパーの記事を書きました。今回も、TODOリストを題材に Re-frame での基本的な書き方を確認してみたいと思います。

Re-frame とは

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

TODO リストを作る

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

仕様は入門 Om Nextの時と同じで、以下となります。

仕様

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

動作確認方法

最終的に完成したコードは github においてあり、以下の方法で動作確認をすることができます。

$ 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 のテンプレートを使いプロジェクトの雛形を作成します。

$ 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

各ファイルの役割

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

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

コード確認

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

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

core.cljs

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

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

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

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

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

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

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 キーは不要のため削除します。

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

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

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 に変更があると更新された値が取得され、コンポーネントが再描画されます。1つ1つの 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

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つで、コンポーネント単位での初期化、ローカル状態の作成などを行う際に利用します。詳しくは公式のドキュメントを確認してみてください。1

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

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/todossubscribe している todo-list が再描画され、TODOの追加が画面のリストに反映されます。

Step3: TODO 完了/取消し

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

step3.png

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 キーを追加しておきます。

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 に記述した対応するイベントハンドラーに依頼します。

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

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 で行います。

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 のサンプルを確認してみると良いと思います。今回触れなかった機能(interceptorとか)やローカルストレージとの連携、spec による状態のバリデーションなどより実践的な使い方を学ぶことができます。

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

参考資料


  1. Creating-Reagent-Component 

  2. Om Next or Re-frame を検討している方にはこちらの記事が参考になると思います。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away