re-frame/docs/SubscriptionsCleanup.md

235 lines
7.0 KiB
Markdown
Raw Permalink Normal View History

## Subscriptions Cleanup
There's a problem and we need to fix it.
### The Problem
The simple example, used in the earlier code walk through, is not idomatic re-frame. It has a flaw.
2017-01-03 11:54:07 +00:00
It does not obey the re-frame rule: **keep views as simple as possible**.
A view shouldn't do any computation on input data. Its job is just to compute hiccup.
The subscriptions it uses should deliver the data already in the right
structure, ready for use in hiccup generation.
### Just Look
2016-12-18 23:53:04 +00:00
Here be the horrors:
```clj
(defn clock
[]
[:div.example-clock
{:style {:color @(rf/subscribe [:time-color])}}
(-> @(rf/subscribe [:time])
.toTimeString
(clojure.string/split " ")
first)])
```
2016-12-19 01:54:18 +00:00
That view obtains data from a `[:time]` subscription and then it
massages that data into the form it needs for use in the hiccup. We don't like that.
### The Solution
Instead, we want to use a new `[:time-str]` subscription which will deliver the data all ready to go, so
the view is 100% concerned with hiccup generation only. Like this:
```clj
(defn clock
[]
[:div.example-clock
{:style {:color @(rf/subscribe [:time-color])}}
@(rf/subscribe [:time-str])])
```
Which, in turn, means we must write this `time-str` subscription handler:
```clj
(reg-sub
:time-str
(fn [_ _]
(subscribe [:time]))
(fn [t _]
(-> t
.toTimeString
(clojure.string/split " ")
first)))
```
Much better.
You'll notice this new subscription handler belongs to the "Level 3"
layer of the reactive flow. See the [Infographic](SubscriptionInfographic.md).
2016-12-18 22:42:16 +00:00
2016-12-19 01:54:18 +00:00
### Another Technique
2016-12-18 22:42:16 +00:00
Above, I suggested this:
2016-12-18 22:42:16 +00:00
```clj
(defn clock
[]
[:div.example-clock
{:style {:color @(rf/subscribe [:time-color])}}
@(rf/subscribe [:time-str])])
```
2017-07-14 03:56:39 +00:00
But that may offend your aesthetics. Too much noise with those two `@`?
2016-12-18 22:42:16 +00:00
2016-12-19 01:54:18 +00:00
To clean this up, we can define a new `listen` function:
2016-12-18 22:42:16 +00:00
```clj
(defn listen
[query-v]
@(rf/subscribe query-v))
2016-12-18 22:42:16 +00:00
```
2016-12-19 01:54:18 +00:00
And then rewrite:
2016-12-18 22:42:16 +00:00
```clj
(defn clock
[]
[:div.example-clock
{:style {:color (listen [:time-color])}}
(listen [:time-str])])
```
2016-12-19 01:54:18 +00:00
So, at the cost of writing your own function, `listen`, the code is now less noisy
AND there's less chance of us forgetting an `@` (which can lead to odd problems).
2016-12-18 22:42:16 +00:00
2017-07-14 03:56:39 +00:00
### LambdaIsland Naming (LIN)
I've ended up quite liking [the alternative names](https://lambdaisland.com/blog/11-02-2017-re-frame-form-1-subscriptions)
suggested by [Lambda Island Videos](https://lambdaisland.com/):
```clj
2017-07-14 03:56:39 +00:00
(def <sub (comp deref re-frame.core/subscribe)) ;; aka listen (above)
(def >evt re-frame.core/dispatch)
```
2016-12-18 22:42:16 +00:00
### Say It Again
So, if, in code review, you saw this view function:
2016-12-18 22:42:16 +00:00
```clj
(defn show-items
[]
(let [sorted-items (sort @(subscribe [:items]))]
(into [:div] (for [i sorted-items] [item-view i]))))
```
2016-12-19 01:54:18 +00:00
What would you (supportively) object to?
2016-12-18 22:42:16 +00:00
2017-07-14 03:56:39 +00:00
That `sort`, right? Computation in the view. Instead, we want exactly the right data
delivered to the view - no further computation required - the view's job is to simply make `hiccup`.
2016-12-18 22:42:16 +00:00
The solution is to create a subscription that delivers items already sorted.
```clj
(reg-sub
:sorted-items
(fn [_ _] (subscribe [:items]))
(fn [items _]
(sort items))
```
Now, in this case the computation is a bit trivial, but the moment it is
a little tricky, you'll want to test it. So separating it out from the
view will make that easier.
2016-12-19 01:54:18 +00:00
To make it testable, you may structure like this:
```clj
(defn item-sorter
[items _]
(sort items))
(reg-sub
:sorted-items
(fn [_ _] (subscribe [:items]))
2016-12-18 23:53:04 +00:00
item-sorter)
```
2016-12-19 01:54:18 +00:00
Now it is easy to test `item-sorter` independently.
### And There's Another Benefit
2017-07-14 03:56:39 +00:00
re-frame de-duplicates signal graph nodes.
If, for example, two views wanted to `(subscribe [:sorted-items])` only the one node
2016-12-18 23:53:04 +00:00
(in the signal graph) would be created. Only one node would be doing that
potentially expensive sorting operation (when items changed) and values from
it would be flowing through to both views.
2017-07-14 03:56:39 +00:00
That sort of efficiency can't happen if this views themselves are doing the `sort`.
2016-12-18 23:53:04 +00:00
### de-duplication
As I described above, two, or more, concurrent subscriptions for the same query will source
reactive updates from the one executing handler - from the one node in the signal graph.
How do we know if two subscriptions are "the same"? Answer: two subscriptions
are the same if their query vectors test `=` to each other.
So, these two subscriptions are *not* "the same": `[:some-event 42]` `[:some-event "blah"]`. Even
though they involve the same event id, `:some-event`, the query vectors do not test `=`.
This feature shakes out nicely because re-frame has a data oriented design.
2017-01-02 21:20:59 +00:00
### A Final FAQ
2017-01-03 11:54:07 +00:00
The following issue comes up a bit.
2017-01-02 21:20:59 +00:00
2017-01-03 11:54:07 +00:00
You will end up with a bunch of level 1 `reg-sub` which
2017-01-03 11:04:45 +00:00
look the same (they directly extract a path within `app-db`):
2017-01-02 21:20:59 +00:00
```clj
(reg-sub
:a
(fn [db _]
(:a db)))
```
```clj
(reg-sub
:b
(fn [db _]
(-> db :top :b)))
```
2017-01-03 11:04:45 +00:00
Now, you think and design abstractly for a living, and that repetition will feel uncomfortable. It will
2017-01-03 11:54:07 +00:00
call to you like a Siren: "refaaaaactoooor meeeee". "Maaaake it DRYYYY".
2017-01-03 12:11:05 +00:00
So here's my tip: tie yourself to the mast and sail on. That repetition is good. It is serving a purpose.
2017-01-03 11:54:07 +00:00
Just sail on.
2017-01-02 21:20:59 +00:00
2017-01-03 12:11:05 +00:00
The WORST thing you can do is to flex your magnificent abstraction muscles
2017-01-03 11:04:45 +00:00
and create something like this:
2017-01-02 21:20:59 +00:00
```clj
(reg-sub
:extract-any-path
(fn [db path]
(get-in db path))
```
2017-01-03 11:54:07 +00:00
"Genius!", you think to yourself. "Now I only need one direct `reg-sub` and I supply a path to it.
A read-only cursor of sorts. Look at the code I can delete."
2017-01-02 21:20:59 +00:00
2017-01-03 11:54:07 +00:00
Neat and minimal it most certainly is, yes, but genius it isn't. You are now asking the
2017-01-02 21:42:19 +00:00
code USING the subscription to provide the path. You have traded some innocuous
2017-01-03 11:04:45 +00:00
repetition for longer term fragility, and that's not a good trade.
2017-01-02 21:42:19 +00:00
2017-01-03 11:54:07 +00:00
What fragility? Well, the view which subscribes using, say, `(subscribe [:extract-any-path [:a]])`
now "knows" about (depends on) the structure within `app-db`.
2017-01-02 21:20:59 +00:00
2017-01-03 11:04:45 +00:00
What happens when you inevitably restructure `app-db` and put that `:a` path under
2017-01-02 21:20:59 +00:00
another high level branch of `app-db`? You will have to run around all the views,
2017-01-03 11:04:45 +00:00
looking for the paths supplied, knowing which to alter and which to leave alone.
Fragile.
2017-01-02 21:20:59 +00:00
2017-01-02 21:42:19 +00:00
We want our views to declaratively ask for data, but they should have
2017-01-03 11:54:07 +00:00
no idea where it comes from. It is the job of a subscription to know where data comes from.
Remember our rule at the top: **keep views as simple as possible**.
Don't give them knowledge or tasks outside their remit.
2017-01-02 21:20:59 +00:00
***
Previous: [Infographic](SubscriptionInfographic.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
2016-12-19 02:01:48 +00:00
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Flow Mechanics](SubscriptionFlow.md) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<!-- END doctoc generated TOC please keep comment here to allow auto update -->