Friday, November 25, 2016

Creating custom effects in re-frame

In previous posts, we learned how to use coeffects and effects in re-frame to have effectful event handlers that are pure functions.

The example with effects we saw, used two of re-frame's built-in effect handlers (dispatch and dispatch-later). Since the set of possible side-effects is open-ended, re-frame gives you a way to define your own effects.

In this post, we'll show how to create your own effect by defining one that writes to the local storage.

Let's have a look at an event handler using that effect that writes to the local storage:

(ns visual-spelling.play.answer.handlers
(:require
[visual-spelling.db :as db]))
(defn word-checked-handler [{:keys [db]} _]
(let [db (db/update-on-word-checked db)]
{:db db
:write-localstore {:visited-words (:visited-words db)
:results (:results db)}}))
Notice the a key-value pair having the key :write-localstore in the effects map returned by the event handler. This key-value pair is telling re-frame to do a side-effect, which is uniquely identified by that key and requires the data in the corresponding value. But, how does re-frame know how to action on the :write-localstore effect?

We have to use re-frame's reg-fx function to associate the effect key, (:write-localstore), with an effect handler, (local-store/write function):

(ns visual-spelling.register-effects
(:require
[re-frame.core :as re-frame]
[visual-spelling.local-store :as local-store]))
(re-frame/reg-fx
:write-localstore local-store/write)
reg-fx receives two arguments: the key that identifies the effect and the function which actions the side-effect, the effect handler.

When an event handler returns an effects map which contains a given effect key, the effect handler associated with it using reg-fx will be called to action on the effect, passing it the value associated to the effect key in the effects map.

The local-store/write function is passed a map containing the key-value pairs it has to write to the local store.

(ns visual-spelling.local-store)
(defn- write-key
[ls-key data]
(.setItem js/localStorage ls-key (str data)))
(defn- load-key [ls-key]
(some->> (.getItem js/localStorage ls-key)
(cljs.reader/read-string)))
(defn write [data-kv]
(doseq [[k v] data-kv]
(write-key k v)))
(defn load [ls-keys]
(reduce
#(assoc %1 %2 (load-key %2))
{}
ls-keys))
Finally, this is how we'd test the event handler using it:

(ns visual-spelling.play.answer.word-checked-handler-test
(:require
[cljs.test :refer-macros [deftest testing is]]
[visual-spelling.test-helpers.builders
:refer [make-word make-raw-word raw-parts parts]]
[visual-spelling.play.answer.handlers :refer [word-checked-handler]]
[visual-spelling.test-helpers.db-builder
:refer [make-db]]
[visual-spelling.test-helpers.handlers-checkers
:refer [check-writing-to-local-storage-contains
check-writing-to-local-storage]]))
(deftest test-right-and-wrong-word-handlers
(let [answer-timestamp :some-ts
correct-word-text "ess"
initial-current-word-index 1
db (make-db
:language :catalan
:current-word (make-word
:status :all-right-answers
:correct-word correct-word-text
:answer-ts answer-timestamp
:parts (parts "e" "s"
{:correct-answer "s"
:user-answer "s"}))
:current-word-index initial-current-word-index
:words {"first-word" (make-raw-word)
"current-word" (make-raw-word
:description "current-word"
:parts (raw-parts
"e"
{:correct-answer "ss"
:options ["c", "ç", "k", "s", "ss", "z", "q"]}))
"next-word" (make-raw-word
:description "next-word"
:parts (raw-parts
"e"
{:correct-answer "ss"
:options ["c", "ç", "k", "s", "ss", "z", "q"]}))})
co-fx {:db db}
resulting-fx (word-checked-handler co-fx :no-event)]
(testing "it saves the visited words in the local storage"
(check-writing-to-local-storage-contains
resulting-fx :visited-words correct-word-text))
(testing "that right word handler saves the user answer to results"
(check-writing-to-local-storage resulting-fx :results))))
being check-writing-to-local-storage-contains and check-writing-to-local-storage:

;; some more code
;;.....
(defn- extract-from-writing-to-local-storage
[fx saved-thing-key]
(-> fx :write-localstore saved-thing-key))
(defn check-writing-to-local-storage
[fx saved-thing-key]
(is (= (extract-from-writing-to-local-storage fx saved-thing-key)
(extract-from-db-in-fx-or-cofx fx saved-thing-key))))
(defn check-writing-to-local-storage-contains
[fx saved-thing-key expected-content]
(is (contains? (extract-from-writing-to-local-storage
fx saved-thing-key)
expected-content)))
;;.....
;; some more code
And that's all we need to know in order to create a custom effect in re-frame.

No comments:

Post a Comment