Kontrol Tingkat Atas dengan Redux State Management: Tutorial ClojureScript
Diterbitkan: 2022-03-11Selamat datang kembali untuk bagian menarik kedua dari Unearthing ClojureScript! Dalam posting ini, saya akan membahas langkah besar berikutnya untuk serius dengan ClojureScript: manajemen negara—dalam hal ini, menggunakan React.
Dengan perangkat lunak front-end, manajemen negara adalah masalah besar. Di luar kotak, ada beberapa cara untuk menangani keadaan di Bereaksi:
- Menjaga status di tingkat atas, dan meneruskannya (atau penangan untuk bagian tertentu) ke komponen anak.
- Melempar kemurnian keluar dari jendela dan memiliki variabel global atau bentuk injeksi ketergantungan Lovecraftian.
Secara umum, tidak satu pun dari ini yang bagus. Menjaga status di tingkat atas cukup sederhana, tetapi kemudian ada sejumlah besar overhead untuk menurunkan status aplikasi ke setiap komponen yang membutuhkannya.
Sebagai perbandingan, memiliki variabel global (atau versi keadaan naif lainnya) dapat mengakibatkan masalah konkurensi yang sulit dilacak, yang menyebabkan komponen tidak diperbarui saat Anda mengharapkannya, atau sebaliknya.
Jadi bagaimana ini bisa diatasi? Bagi Anda yang akrab dengan React, Anda mungkin telah mencoba Redux, wadah negara untuk aplikasi JavaScript. Anda mungkin menemukan ini atas kemauan Anda sendiri, dengan berani mencari sistem yang dapat dikelola untuk mempertahankan status. Atau Anda mungkin baru saja menemukannya saat membaca tentang JavaScript dan alat web lainnya.
Terlepas dari bagaimana orang akhirnya melihat Redux, menurut pengalaman saya, mereka biasanya berakhir dengan dua pemikiran:
- “Saya merasa harus menggunakan ini karena semua orang mengatakan bahwa saya harus menggunakannya.”
- “Saya tidak sepenuhnya mengerti mengapa ini lebih baik.”
Secara umum, Redux menyediakan abstraksi yang memungkinkan manajemen negara sesuai dengan sifat reaktif dari React. Dengan melepas semua statefulness ke sistem seperti Redux, Anda menjaga kemurnian React. Dengan demikian Anda akan berakhir dengan sakit kepala yang jauh lebih sedikit dan umumnya sesuatu yang jauh lebih mudah untuk dipikirkan.
Bagi Mereka yang Baru mengenal Clojure
Meskipun ini mungkin tidak membantu Anda mempelajari ClojureScript sepenuhnya dari awal, di sini saya setidaknya akan merangkum beberapa konsep keadaan dasar di Clojure[Script]. Jangan ragu untuk melewatkan bagian ini jika Anda sudah menjadi Clojurian berpengalaman!
Ingat salah satu dasar-dasar Clojure yang berlaku untuk ClojureScript juga: Secara default, data tidak dapat diubah. Ini bagus untuk mengembangkan dan memiliki jaminan bahwa apa yang Anda buat pada timestep N masih sama pada timestep > N. ClojureScript juga memberi kita cara mudah untuk memiliki status yang bisa berubah jika kita membutuhkannya, melalui konsep atom
.
Sebuah atom
di ClojureScript sangat mirip dengan AtomicReference
di Java: Ini menyediakan objek baru yang mengunci isinya dengan jaminan konkurensi. Sama seperti di Java, Anda dapat menempatkan apa pun yang Anda suka di objek ini—sejak saat itu, atom itu akan menjadi referensi atom untuk apa pun yang Anda inginkan.
Setelah Anda memiliki atom
Anda, Anda dapat secara atom menetapkan nilai baru ke dalamnya dengan menggunakan reset!
function (perhatikan !
dalam fungsi—dalam bahasa Clojure ini sering digunakan untuk menandakan bahwa suatu operasi adalah stateful atau tidak murni).
Perhatikan juga bahwa—tidak seperti Java—Clojure tidak peduli apa yang Anda masukkan ke dalam atom
. Itu bisa berupa string, daftar, atau objek. Mengetik dinamis, sayang!
(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 :)
Reagen memperluas konsep atom ini dengan atom
sendiri. (Jika Anda tidak terbiasa dengan Reagent, periksa posting sebelum ini.) Ini berperilaku identik dengan atom
ClojureScript , kecuali itu juga memicu peristiwa render di Reagen, seperti penyimpanan status bawaan React.
Sebuah contoh:
(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!")}]])
Ini akan menampilkan satu <div>
yang berisi <span>
mengatakan "Halo, dunia!" dan tombol, seperti yang Anda harapkan. Menekan tombol itu akan secara atom mengubah my-atom
menjadi berisi "there!"
. Itu akan memicu penggambaran ulang komponen, menghasilkan rentang yang mengatakan "Halo, di sana!" alih-alih.
Ini tampaknya cukup sederhana untuk mutasi tingkat komponen lokal, tetapi bagaimana jika kita memiliki aplikasi yang lebih rumit yang memiliki banyak tingkat abstraksi? Atau jika kita perlu berbagi status umum antara beberapa sub-komponen, dan sub-komponennya?
Contoh yang Lebih Rumit
Mari kita telusuri ini dengan sebuah contoh. Di sini kita akan menerapkan halaman login mentah:
(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)}]])
Kami kemudian akan meng-host komponen login ini dalam app.cljs
utama kami, seperti:
(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]])
Alur kerja yang diharapkan adalah sebagai berikut:
- Kami menunggu pengguna memasukkan nama pengguna dan kata sandi mereka dan tekan kirim.
- Ini akan memicu fungsi
do-login-io
kita di komponen induk. - Fungsi
do-login-io
melakukan beberapa operasi I/O (seperti masuk ke server dan mengambil token).
Jika operasi ini memblokir, maka kita sudah berada dalam tumpukan masalah, karena aplikasi kita dibekukan—jika tidak, maka kita harus khawatir tentang asinkron!
Selain itu, sekarang kami perlu memberikan token ini ke semua sub-komponen kami yang ingin melakukan kueri ke server kami. Refactoring kode menjadi jauh lebih sulit!
Akhirnya, komponen kami sekarang tidak lagi sepenuhnya reaktif — sekarang terlibat dalam mengelola status aplikasi lainnya, memicu I/O dan umumnya menjadi sedikit gangguan.
Tutorial ClojureScript: Masuk ke Redux
Redux adalah tongkat ajaib yang membuat semua impian Anda yang berbasis negara menjadi kenyataan. Diimplementasikan dengan benar, ini menyediakan abstraksi berbagi status yang aman, cepat, dan mudah digunakan.
Cara kerja Redux (dan teori di baliknya) agak di luar cakupan artikel ini. Sebagai gantinya, saya akan menyelami contoh kerja dengan ClojureScript, yang diharapkan dapat menunjukkan kemampuannya!
Dalam konteks kami, Redux diimplementasikan oleh salah satu dari banyak perpustakaan ClojureScript yang tersedia; yang ini namanya re-frame. Ini menyediakan pembungkus Clojure-ified di sekitar Redux yang (menurut saya) membuatnya sangat menyenangkan untuk digunakan.
Dasar
Redux menaikkan status aplikasi Anda, membuat komponen Anda ringan. Komponen Reduxified hanya perlu memikirkan:
- Seperti apa rupanya
- Data apa yang dikonsumsi
- Peristiwa apa yang dipicunya
Sisanya ditangani di belakang layar.
Untuk menekankan hal ini, mari kita Reduxify halaman login kita di atas.
Data
Hal pertama yang pertama: Kita perlu memutuskan seperti apa model aplikasi kita nantinya. Kami melakukan ini dengan menentukan bentuk data kami, data yang akan dapat diakses di seluruh aplikasi.

Aturan praktis yang baik adalah bahwa jika data perlu digunakan di beberapa komponen Redux, atau perlu berumur panjang (seperti token kami), maka itu harus disimpan dalam database. Sebaliknya, jika data bersifat lokal untuk komponen (seperti bidang nama pengguna dan sandi) maka data tersebut harus hidup sebagai status komponen lokal dan tidak disimpan dalam database.
Mari buat boilerplate database kita dan tentukan token kita:
(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})
Ada beberapa poin menarik yang perlu diperhatikan di sini:
- Kami menggunakan perpustakaan
spec
Clojure untuk menggambarkan bagaimana data kami seharusnya terlihat. Ini sangat sesuai dalam bahasa dinamis seperti Clojure[Script]. - Untuk contoh ini, kami hanya melacak token global yang akan mewakili pengguna kami setelah mereka masuk. Token ini adalah string sederhana.
- Namun, sebelum pengguna masuk, kami tidak akan memiliki token. Ini diwakili oleh kata kunci
:opt-un
, yang merupakan singkatan dari “opsional, tidak memenuhi syarat.” (Di Clojure, kata kunci biasa akan seperti:cat
, sedangkan kata kunci yang memenuhi syarat mungkin seperti:animal/cat
. Kualifikasi biasanya terjadi di tingkat modul—ini menghentikan kata kunci di modul yang berbeda agar tidak saling mengalahkan.) - Akhirnya, kami menentukan status default database kami, yang merupakan cara inisialisasi.
Kapan saja, kita harus yakin bahwa data dalam database kita cocok dengan spesifikasi kita di sini.
Langganan
Sekarang kita telah menggambarkan model data kita, kita perlu mencerminkan bagaimana pandangan kita menunjukkan data itu. Kami telah menjelaskan seperti apa tampilan kami di komponen Redux kami — sekarang kami hanya perlu menghubungkan tampilan kami ke database kami.
Dengan Redux, kami tidak mengakses database kami secara langsung—ini dapat mengakibatkan masalah siklus hidup dan konkurensi. Sebagai gantinya, kami mendaftarkan hubungan kami dengan aspek basis data melalui langganan .
Langganan memberi tahu re-frame (dan Reagen) bahwa kami bergantung pada bagian dari database, dan jika bagian itu diubah, maka komponen Redux kami harus dirender ulang.
Langganan sangat sederhana untuk didefinisikan:
(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)
Di sini, kami mendaftarkan satu langganan—ke token itu sendiri. Langganan hanyalah nama langganan, dan fungsi yang mengekstrak item tersebut dari database. Kami dapat melakukan apa pun yang kami inginkan untuk nilai itu, dan mengubah tampilan sebanyak yang kami suka di sini; namun, dalam kasus ini, kami hanya mengekstrak token dari database dan mengembalikannya.
Masih banyak lagi yang dapat Anda lakukan dengan langganan—seperti mendefinisikan tampilan pada subbagian database untuk cakupan yang lebih ketat pada rendering ulang—tetapi kami akan membuatnya tetap sederhana untuk saat ini!
Acara
Kami memiliki database kami, dan kami memiliki pandangan kami ke dalam database. Sekarang kita perlu memicu beberapa peristiwa! Dalam contoh ini, kami memiliki dua jenis acara:
- Peristiwa murni ( tidak memiliki efek samping) dari penulisan token baru ke dalam database.
- Acara I/O ( memiliki efek samping) keluar dan meminta token kami melalui beberapa interaksi klien.
Kita akan mulai dengan yang mudah. Re-frame bahkan menyediakan fungsi persis untuk acara semacam ini:
(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)))
Sekali lagi, ini cukup mudah di sini—kami telah mendefinisikan dua peristiwa. Yang pertama adalah untuk menginisialisasi database kita. (Lihat bagaimana ia mengabaikan kedua argumennya? Kami selalu menginisialisasi database dengan default-db
kami!) Yang kedua adalah untuk menyimpan token kami setelah kami mendapatkannya.
Perhatikan bahwa tak satu pun dari peristiwa ini memiliki efek samping—tidak ada panggilan eksternal, tidak ada I/O sama sekali! Ini sangat penting untuk menjaga kesucian proses Redux suci. Jangan membuatnya tidak murni agar Anda tidak menginginkan murka Redux atas Anda.
Akhirnya, kita membutuhkan acara login kita. Kami akan menempatkannya di bawah yang lain:
(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]))))
Fungsi reg-event-fx
sebagian besar mirip dengan reg-event-db
, meskipun ada beberapa perbedaan halus.
- Argumen pertama tidak lagi hanya database itu sendiri. Ini berisi banyak hal lain yang dapat Anda gunakan untuk mengelola status aplikasi.
- Argumen kedua sangat mirip dengan
reg-event-db
. - Daripada hanya mengembalikan
db
baru, kami malah mengembalikan peta yang mewakili semua efek (“fx”) yang seharusnya terjadi untuk acara ini. Dalam hal ini, kita cukup memanggil efek:request-token
, yang didefinisikan di bawah ini. Salah satu efek valid lainnya adalah:dispatch
, yang hanya memanggil acara lain.
Setelah efek kami dikirim, efek :request-token
kami dipanggil, yang melakukan operasi login I/O kami yang sudah berjalan lama. Setelah ini selesai, dengan senang hati mengirimkan hasilnya kembali ke loop acara, sehingga menyelesaikan siklus!
Tutorial ClojureScript: Hasil Akhir
Jadi! Kami telah mendefinisikan abstraksi penyimpanan kami. Seperti apa komponen tersebut sekarang?
(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]})}]])
Dan komponen aplikasi kami:
(ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- VIEW -- (defn component [] [:div [login/component]])
Dan akhirnya, mengakses token kami di beberapa komponen jarak jauh sesederhana:
(let [token @(rf/subscribe [:token])] ; ... )
Menyatukan semuanya:
Tidak ada keributan, tidak ada keributan.
Memisahkan Komponen dengan Redux/Re-frame Berarti Manajemen Keadaan Bersih
Menggunakan Redux (melalui re-frame), kami berhasil memisahkan komponen tampilan kami dari kekacauan penanganan keadaan. Memperluas abstraksi negara kita sekarang menjadi mudah!
Redux di ClojureScript sangat mudah —Anda tidak punya alasan untuk tidak mencobanya.
Jika Anda siap untuk melakukan cracking, saya sarankan untuk memeriksa dokumen re-frame yang fantastis dan contoh sederhana kami yang berhasil. Saya berharap untuk membaca komentar Anda tentang tutorial ClojureScript di bawah ini. Semoga berhasil!