Redux状態管理によるトップレベルの制御:ClojureScriptチュートリアル
公開: 2022-03-11Unearthing ClojureScriptの2回目のエキサイティングな記事へようこそ! この投稿では、ClojureScriptで真剣に取り組むための次の大きなステップである状態管理(この場合はReactを使用)について説明します。
フロントエンドソフトウェアでは、状態管理は重要です。 すぐに使用できる、Reactで状態を処理する方法はいくつかあります。
- 状態を最上位に保持し、それ(または特定の状態のハンドラー)を子コンポーネントに渡します。
- 純粋さを窓の外に投げ出し、グローバル変数または何らかのラブクラフト形式の依存性注入を行う。
一般的に言って、これらはどちらも素晴らしいものではありません。 状態をトップレベルに維持することは非常に簡単ですが、アプリケーションの状態をそれを必要とするすべてのコンポーネントに渡すには、かなりのオーバーヘッドがあります。
比較すると、グローバル変数(または他の単純なバージョンの状態)があると、並行性の問題を追跡するのが難しくなり、コンポーネントが期待どおりに更新されない、またはその逆になる可能性があります。
では、これにどのように取り組むことができますか? Reactに精通している方は、JavaScriptアプリの状態コンテナーであるReduxを試してみたことがあるかもしれません。 状態を維持するための管理可能なシステムを大胆に探して、あなたはあなた自身の意志からこれを見つけたかもしれません。 または、JavaScriptやその他のWebツールについて読んでいるときに偶然見つけたかもしれません。
人々がどのようにReduxを見るようになるかに関係なく、私の経験では、彼らは一般的に2つの考えに終わります。
- 「誰もが私がそれを使わなければならないと言っているので、私はこれを使わなければならないような気がします。」
- 「なぜこれが優れているのか、私は完全には理解していません。」
一般的に、Reduxは、状態管理をReactのリアクティブな性質に適合させる抽象化を提供します。 すべてのステートフルネスをReduxのようなシステムにオフロードすることで、Reactの純度を維持できます。 したがって、頭痛の種がはるかに少なくなり、一般的には推論がはるかに簡単になります。
Clojureを初めて使用する人のために
これはClojureScriptを完全にゼロから学ぶのに役立たないかもしれませんが、ここでは少なくともClojure[Script]のいくつかの基本的な状態の概念を要約します。 すでにベテランのクロジュリアンである場合は、これらの部分をスキップしてください。
ClojureScriptにも適用されるClojureの基本の1つを思い出してください。デフォルトでは、データは不変です。 これは、タイムステップNで作成したものがタイムステップ> Nでも同じであることを開発および保証するのに最適です。また、ClojureScriptは、 atom
の概念を介して、必要に応じて変更可能な状態を設定する便利な方法を提供します。
ClojureScriptのatom
は、JavaのAtomicReference
と非常によく似ています。これは、同時実行性を保証してコンテンツをロックする新しいオブジェクトを提供します。 Javaの場合と同様に、このオブジェクトには好きなものを配置できます。それ以降、そのアトムは必要なものへのアトミック参照になります。
atom
を取得したら、 reset!
を使用して新しい値をアトミックに設定できます。 関数(関数の!
に注意してください。Clojure言語では、これは、操作がステートフルまたは不純であることを示すためによく使用されます)。
また、Javaとは異なり、Clojureはatom
に何を入れてもかまいません。 文字列、リスト、またはオブジェクトである可能性があります。 動的型付け、ベイビー!
(def my-mutable-map (atom {})) ; recall that {} means an empty map in Clojure (println @my-mutable-map) ; You 'dereference' an atom using @ ; -> this prints {} (reset! my-mutable-map {:hello "there"}) ; atomically set the atom (reset! my-mutable-map "hello, there!") ; don't forget Clojure is dynamic :)
試薬は、アトムのこの概念を独自のatom
で拡張します。 (Reagentに慣れていない場合は、この前の投稿を確認してください。)これは、Reactの組み込みの状態ストアと同様に、Reagentでレンダリングイベントをトリガーすることを除いて、ClojureScript atom
と同じように動作します。
例:
(ns example (:require [reagent.core :refer [atom]])) ; in this module, atom now refers ; to reagent's atom. (def my-atom (atom "world!")) (defn component [] [:div [:span "Hello, " @my-atom] [:input {:type "button" :value "Press Me!" :on-click #(reset! My-atom "there!")}]])
これにより、「Hello、world!」という<span>
を含む単一の<div>
が表示されます。 ご想像のとおり、ボタン。 そのボタンを押すとmy-atom
がアトミックに変異して"there!"
含まれるようになります。 。 これにより、コンポーネントの再描画がトリガーされ、スパンに「こんにちは、そこに!」と表示されます。 代わりは。
これは、ローカルのコンポーネントレベルの変更には十分単純に見えますが、複数のレベルの抽象化を備えたより複雑なアプリケーションがある場合はどうでしょうか。 または、複数のサブコンポーネントとそれらのサブコンポーネント間で共通の状態を共有する必要がある場合はどうでしょうか。
より複雑な例
例を挙げてこれを調べてみましょう。 ここでは、大まかなログインページを実装します。
(ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [on-login] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(on-login @username @password)}]])
次に、次のように、このログインコンポーネントをメインのapp.cljs
内でホストします。
(ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- STATE (def token (atom nil)) ;; -- LOGIC -- (defn- do-login-io [username password] (let [t (complicated-io-login-operation username password)] (reset! token t))) ;; -- VIEW -- (defn component [] [:div [login/component do-login-io]])
したがって、予想されるワークフローは次のとおりです。
- ユーザーがユーザー名とパスワードを入力して送信を押すのを待ちます。
- これにより、親コンポーネントで
do-login-io
関数がトリガーされます。 -
do-login-io
関数は、いくつかのI / O操作(サーバーへのログインやトークンの取得など)を実行します。
この操作がブロックされている場合は、アプリケーションがフリーズしているため、すでに問題が山積しています。フリーズしていない場合は、非同期で心配する必要があります。
さらに、サーバーへのクエリを実行するすべてのサブコンポーネントにこのトークンを提供する必要があります。 コードのリファクタリングがさらに難しくなりました。
最後に、コンポーネントは純粋にリアクティブではなくなりました。アプリケーションの残りの部分の状態の管理に加担し、I / Oをトリガーし、一般的に少し厄介です。
ClojureScriptチュートリアル:Reduxを入力してください
Reduxは、州ベースのすべての夢を実現する魔法の杖です。 適切に実装されると、安全で高速で使いやすい状態共有の抽象化が提供されます。
Reduxの内部動作(およびその背後にある理論)は、この記事の範囲外です。 代わりに、ClojureScriptを使用した実例に飛び込みます。これは、ClojureScriptの機能を実証するために何らかの方法で役立つはずです。
私たちのコンテキストでは、Reduxは利用可能な多くのClojureScriptライブラリの1つによって実装されています。 これはリフレームと呼ばれます。 これは、Reduxの周りにClojure化されたラッパーを提供し、(私の意見では)それを使用することを絶対に喜ばせます。
基礎
Reduxはアプリケーションの状態を引き上げ、コンポーネントを軽量にします。 Reduxifiedコンポーネントは、次のことだけを考慮する必要があります。
- それはどのようなものか
- 消費するデータ
- トリガーするイベント
残りは舞台裏で処理されます。
この点を強調するために、上記のログインページを再利用してみましょう。
データベース
まず最初に:アプリケーションモデルがどのようになるかを決定する必要があります。 これを行うには、データの形状、つまりアプリ全体でアクセスできるデータを定義します。
経験則として、データを複数のReduxコンポーネントで使用する必要がある場合、またはデータを長期間使用する必要がある場合(トークンのように)、データベースに保存する必要があります。 対照的に、データがコンポーネントに対してローカルである場合(ユーザー名やパスワードのフィールドなど)、データはローカルコンポーネントの状態として存在し、データベースに保存されないようにする必要があります。

データベースの定型文を作成し、トークンを指定してみましょう。
(ns unearthing-clojurescript.state.db (:require [cljs.spec.alpha :as s] [re-frame.core :as re-frame])) (s/def ::token string?) (s/def ::db (s/keys :opt-un [::token])) (def default-db {:token nil})
ここで注目に値するいくつかの興味深い点があります:
- Clojureの
spec
ライブラリを使用して、データがどのように表示されるかを記述します。 これは、Clojure[Script]のような動的言語で特に適切です。 - この例では、ユーザーがログインするとユーザーを表すグローバルトークンのみを追跡します。このトークンは単純な文字列です。
- ただし、ユーザーがログインする前は、トークンはありません。 これは、「オプション、非修飾」を表す
:opt-un
キーワードで表されます。 (Clojureでは、通常のキーワードは:cat
のようになりますが、修飾されたキーワードは:animal/cat
のようになります。修飾は通常、モジュールレベルで行われます。これにより、異なるモジュールのキーワードが互いに混ざり合うのを防ぎます。) - 最後に、データベースのデフォルト状態を指定します。これは、データベースが初期化される方法です。
どの時点でも、データベース内のデータがここでの仕様と一致していることを確認する必要があります。
サブスクリプション
データモデルについて説明したので、ビューがそのデータをどのように表示するかを反映する必要があります。 Reduxコンポーネントでビューがどのように表示されるかについてはすでに説明しました。次に、ビューをデータベースに接続するだけです。
Reduxでは、データベースに直接アクセスしません。これにより、ライフサイクルと同時実行性の問題が発生する可能性があります。 代わりに、サブスクリプションを通じてデータベースのファセットとの関係を登録します。
サブスクリプションは、データベースの一部に依存していることをリフレーム(および試薬)に通知します。その部分が変更された場合は、Reduxコンポーネントを再レンダリングする必要があります。
サブスクリプションの定義は非常に簡単です。
(ns unearthing-clojurescript.state.subs (:require [re-frame.core :refer [reg-sub]])) (reg-sub :token ; <- the name of the subscription (fn [{:keys [token] :as db} _] ; first argument is the database, second argument is any token)) ; args passed to the subscribe function (not used here)
ここでは、トークン自体に単一のサブスクリプションを登録します。 サブスクリプションは、単にサブスクリプションの名前であり、データベースからそのアイテムを抽出する関数です。 その値に対してやりたいことは何でもでき、ここで好きなだけビューを変更できます。 ただし、この場合は、データベースからトークンを抽出して返すだけです。
再レンダリングの範囲を狭めるためにデータベースのサブセクションでビューを定義するなど、サブスクリプションでできることははるかにたくさんありますが、今のところは単純なままにしておきます。
イベント
私たちにはデータベースがあり、データベースに対する見方もあります。 次に、いくつかのイベントをトリガーする必要があります。 この例では、2種類のイベントがあります。
- データベースに新しいトークンを書き込む純粋なイベント(副作用はありません)。
- 外出してクライアントとのやり取りを通じてトークンを要求するI/Oイベント(副作用があります)。
簡単なものから始めましょう。 リフレームは、この種のイベントにぴったりの機能も提供します。
(ns unearthing-clojurescript.state.events (:require [re-frame.core :refer [reg-event-db reg-event-fx reg-fx] :as rf] [unearthing-clojurescript.state.db :refer [default-db]])) ; our start up event that initialises the database. ; we'll trigger this in our core.cljs (reg-event-db :initialise-db (fn [_ _] default-db)) ; a simple event that places a token in the database (reg-event-db :store-login (fn [db [_ token]] (assoc db :token token)))
ここでも、非常に簡単です。2つのイベントを定義しました。 1つ目は、データベースを初期化するためのものです。 (両方の引数を無視する方法を参照してください。常にdefault-db
でデータベースを初期化します!)2つ目は、トークンを取得したらトークンを格納するためのものです。
これらのイベントにはどちらも副作用がないことに注意してください。外部呼び出しもI/Oもまったくありません。 これは、聖なるReduxプロセスの神聖さを維持するために非常に重要です。 Reduxの怒りをあなたに望まないように、それを不純にしないでください。
最後に、ログインイベントが必要です。 他の下に配置します:
(reg-event-fx :login (fn [{:keys [db]} [_ credentials]] {:request-token credentials})) (reg-fx :request-token (fn [{:keys [username password]}] (let [token (complicated-io-login-operation username password)] (rf/dispatch [:store-login token]))))
reg-event-fx
関数は、 reg-event-db
とほぼ同じですが、微妙な違いがいくつかあります。
- 最初の引数は、もはやデータベース自体だけではありません。 これには、アプリケーションの状態を管理するために使用できる他の多くのものが含まれています。
- 2番目の引数は、
reg-event-db
とよく似ています。 - 新しい
db
を返すだけでなく、このイベントで発生するはずのすべての効果( "fx")を表すマップを返します。 この場合、以下に定義する:request-token
エフェクトを単に呼び出します。 他の有効な効果の1つは:dispatch
で、これは単に別のイベントを呼び出します。
エフェクトがディスパッチされると、 :request-token
エフェクトが呼び出され、長時間実行されるI/Oログイン操作が実行されます。 これが完了すると、結果がイベントループに戻され、サイクルが完了します。
ClojureScriptチュートリアル:最終結果
それで! ストレージの抽象化を定義しました。 コンポーネントは現在どのようになっていますか?
(ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]] [re-frame.core :as rf])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(rf/dispatch [:login {:username @username :password @password]})}]])
そして私たちのアプリコンポーネント:
(ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- VIEW -- (defn component [] [:div [login/component]])
そして最後に、いくつかのリモートコンポーネントでトークンにアクセスするのは次のように簡単です。
(let [token @(rf/subscribe [:token])] ; ... )
すべてを一緒に入れて:
大騒ぎも大騒ぎもありません。
コンポーネントをRedux/Re-frameで分離すると、クリーンな状態管理が可能になります
Reduxを使用して(リフレームを介して)、ビューコンポーネントを状態処理の混乱からうまく切り離しました。 状態の抽象化を拡張することは、今や簡単なことです。
ClojureScriptのReduxは本当に簡単です—試してみない理由はありません。
クラッキングを行う準備ができている場合は、素晴らしいリフレームドキュメントと簡単な実例を確認することをお勧めします。 以下のこのClojureScriptチュートリアルに関するコメントを読むのを楽しみにしています。 頑張ってください!