Skip to content

Commit 16eb5d4

Browse files
committed
[docs] Polish & update
1 parent 3a1b703 commit 16eb5d4

File tree

3 files changed

+119
-80
lines changed

3 files changed

+119
-80
lines changed

docs/Flows.md

+43-49
Original file line numberDiff line numberDiff line change
@@ -335,21 +335,7 @@ For this, we use a `:live?` function.
335335
The quote above deals with phenomenal life, but you can also think of `:live?` as in a tv or internet broadcast.
336336
Data flows, but only when the flow itself is live.
337337

338-
Here's another room area flow:
339-
340-
<div class="cm-doc" data-cm-doc-result-format="pass-fail">
341-
(rf/reg-flow
342-
{:id :kitchen-area
343-
:inputs {:w [:kitchen :width]
344-
:h [:kitchen :length]}
345-
:output (fn [{:keys [w h]}] (* w h))
346-
:path [:kitchen :area]
347-
:live-inputs {:tab [:tab]}
348-
:live? (fn [{:keys [tab]}]
349-
(= tab :kitchen))})
350-
</div>
351-
352-
A barebones tab picker, and something to show us the value of `app-db`:
338+
Let's try it out. For example, here's a barebones tab picker, and something to show us the value of `app-db`:
353339

354340
<div class="cm-doc">
355341
(def tabs [:kitchen :garage])
@@ -382,15 +368,25 @@ A barebones tab picker, and something to show us the value of `app-db`:
382368

383369
### Live?
384370

385-
Here's a more advanced version of our kitchen calculator flow.
386-
This replaces our first `:kitchen-area` flow, since it has the same `:id`:
387-
371+
Here's a more advanced version of our room calculator flow.
388372

389-
Notice the different types of inputs. `:w [:kitchen :width]` represents an input as an `app-db` path, while `:tab :current-tab` identifies the value from the `:current-tab` flow we defined earlier.
373+
<div class="cm-doc" data-cm-doc-result-format="pass-fail">
374+
(rf/reg-flow
375+
{:id :kitchen-area
376+
:inputs {:w [:kitchen :width]
377+
:h [:kitchen :length]}
378+
:output (fn [{:keys [w h]}] (* w h))
379+
:path [:kitchen :area]
380+
:live-inputs {:tab [:tab]}
381+
:live? (fn [{:keys [tab]}]
382+
(= tab :kitchen))})
383+
</div>
390384

391-
Also, notice the new `:tab` input, and the new `:live?`.
385+
Notice the new `:live-inputs` and `:live?` keys.
386+
Just like `:output`, `:live:?` is a function of the resolved `:live-inputs`.
392387

393-
Just like `:output`, `:live:?` is a function of `app-db` and the `:inputs`. Re-frame only calculates the `:output` when the `:live?` function returns a truthy value. Otherwise, the flow is presumed dead.
388+
Re-frame only calculates the `:output` when the `:live?` function returns a truthy value.
389+
Otherwise, the flow is presumed dead.
394390

395391
Let's test it out:
396392

@@ -410,7 +406,8 @@ Let's test it out:
410406

411407
<div id="tabbed-app"></div>
412408

413-
Try switching tabs. Notice how `:area` only exists when you're in the `room-calculator` tab. What's happening here?
409+
Try switching tabs.
410+
Notice how the path `[:kitchen :area]` only exists when you're in the `room-calculator` tab. What's happening here?
414411

415412
### Lifecycle
416413

@@ -419,14 +416,12 @@ Depending on the return value of `:live?`, re-frame handles one of 4 possible st
419416

420417
| transition | action |
421418
|---|---|
422-
| From **live** to **live** | run `:output` |
423-
| From **dead** to **live** | run `:init` and `:output` |
419+
| From **live** to **live** | run `:output` (when `:inputs` have changed) |
420+
| From **dead** to **live** | run `:output` |
424421
| From **live** to **dead** | run `:cleanup` |
425422
| From **dead** to **dead** | do nothing |
426423

427-
Basically, *living* flows get output, *dying* flows get cleaned up, *arising* flows get initiated and output.
428-
429-
And independently of all this, `:output` only runs when `:inputs` have changed value.
424+
Basically, *arising* flows get output, *living* flows get output as needed, and *dying* flows get cleaned up.
430425

431426
### Cleanup
432427

@@ -457,32 +452,32 @@ Not only do flows have a lifecycle (defined by `:live?`, `:init` and `:cleanup`)
457452

458453
Here's another demonstration. Think of it as a stripped-down todomvc.
459454
You can add and remove items in a list:
455+
460456
<div class="cm-doc">
461-
(rf/reg-sub ::items :-> (comp reverse ::items))
457+
(rf/reg-sub :items :-> (comp reverse :items))
462458

463459
(rf/reg-event-db
464460
::add-item
465-
(fn [db [_ id]] (update db ::items conj id)))
461+
(fn [db [_ id]] (update db :items conj id)))
466462

467463
(rf/reg-event-db
468464
::delete-item
469-
(fn [db [_ id]] (update db ::items #(remove #{id} %))))
465+
(fn [db [_ id]] (update db :items #(remove #{id} %))))
470466

471467
(defn item [id] [:div "Item" id])
472468

473469
(defn items []
474-
(into [:div] (map item) @(rf/subscribe [::items])))
470+
(into [:div] (map item) @(rf/subscribe [:items])))
475471

476472
(defn controls []
477-
(let [id (atom 0)]
478-
(fn []
479-
[:div
480-
[:span {:style clickable
481-
:on-click #(do (rf/dispatch [::add-item (inc @id)])
482-
(swap! id inc))} "Add"] " "
483-
[:span {:style clickable
484-
:on-click #(do (rf/dispatch [::delete-item @id])
485-
(swap! id dec))} "Delete"] " "])))
473+
(let [id (or (apply max @(rf/subscribe [:items])) 0)]
474+
[:div
475+
[:span {:style clickable
476+
:on-click #(rf/dispatch [::add-item (inc id)])}
477+
"Add"] " "
478+
[:span {:style clickable
479+
:on-click #(rf/dispatch [::delete-item id])}
480+
"Delete"] " "]))
486481

487482
(defonce item-counter-basic-root
488483
(rdc/create-root (js/document.getElementById "item-counter-basic")))
@@ -516,10 +511,9 @@ It builds a flow that validates our item list against the requirements:
516511

517512
<div class="cm-doc" data-cm-doc-result-format="pass-fail">
518513
(defn error-state-flow [{:keys [min-items max-items] :as requirements}]
519-
{:id ::error-state
520-
:path [::error-state]
521-
:inputs {:items [::items]
522-
:tab (rf/flow<- :current-tab)}
514+
{:id :error-state
515+
:path [:error-state]
516+
:inputs {:items [:items]}
523517
:output (fn [{:keys [items]}]
524518
(let [ct (count items)]
525519
(cond
@@ -535,15 +529,15 @@ And register a flow that fits our base requirements:
535529
</div>
536530

537531
Now this flow is calculating an error-state value, and adding it to `app-db` after every event.
538-
This happens as long as the `::items` have changed... right?
539-
Actually, there's another way to make a flow recalculate - we can reregister it.
532+
This happens as long as the `:items` have changed... right?
533+
Actually, there's another way to make a flow recalculate - we can re-register it.
540534

541535
Let's update the app to display our new error state:
542536

543537
<div class="cm-doc">
544538

545539
(defn warning []
546-
(let [error-state (rf/sub :flow {:id ::error-state})]
540+
(let [error-state (rf/sub :flow {:id :error-state})]
547541
[:div {:style {:color "red"}}
548542
(->> @error-state
549543
(get {:too-many "Too many items. Please remove one."
@@ -594,9 +588,9 @@ And a corresponding event, which triggers our `:reg-flow` effect:
594588
What happens after `:reg-flow` runs? Are there now two flows? Actually, no.
595589

596590
- If you register a new flow with the same `:id`, it replaces the old one.
597-
- When we trigger `[:reg-flow (error-state-flow ...)]`
591+
- When we trigger `[:reg-flow (error-state-flow ...)]`:
598592
- The old `:error-state` flow runs `:cleanup`
599-
- The new `:error-state` flow runs `:init` and `:output`
593+
- The new `:error-state` flow runs `:output`
600594

601595
Not only does changing the inputs lead to new output, but so does changing the flow itself.
602596
Let's test it out:

docs/flows-advanced-topics.md

+75-30
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,54 @@ Introducing yet another demo app! Turns out, we were measuring the kitchen to fi
5151
:ct num-bags-to-buy}]]})))
5252
</div>
5353

54-
How can we get a correct value for `num-balloons-to-fill-kitchen`? You might try calling `(rf/subscribe [::num-balloons-to-fill-kitchen])`, but re-frame comes back with a warning about reactive context, and memory leaks... oh my!
54+
How can we get a correct value for `num-balloons-to-fill-kitchen`?
55+
You might try calling `(rf/subscribe [::num-balloons-to-fill-kitchen])`, but re-frame comes back with a warning about reactive context,
56+
and memory leaks... oh my!
5557

5658
### Reactive context
5759

58-
We express some [business logic in subscriptions](https://github.com/day8/re-frame/issues/753), and some in events, but they're not really compatible.
59-
Between subscriptions and events, there is a [coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/).
60+
To know if a thing has changed, you have to remember what it was.
61+
To propagate change from one value to the next, you have to remember the relationship between them (a [`watchable`](https://clojuredocs.org/clojure.core/add-watch)).
62+
Remembering is a side-effect.
6063

61-
Subscriptions can only be accessed within a [reactive context](/re-frame/FAQs/UseASubscriptionInAnEventHandler).
62-
Since an event handler isn't reactive, it can't access any subscriptions.
64+
Reagent does this. Its main constructs - *reactive atom*, and *component* - are stateful, impure.
65+
We depend on this memory. It abstracts the essential complexity of reactive programming.
6366

64-
Furthermore, subscriptions have an `input-signals` function. This allows the value of one subscription to flow into another. But events have no such thing.
67+
Reagent manages atoms and components with an event loop. Only in the context of this loop can be sure reagent's memory is consistent.
68+
Literally, this is called [`*ratom-context*`](https://github.com/reagent-project/reagent/blob/a14faba55e373000f8f93edfcfce0d1222f7e71a/src/reagent/ratom.cljs#L12).
6569

66-
That means, to get a usable value for `num-balloons-to-fill-kitchen`, we have to duplicate the business logic that we wrote into our subscription, along with the *entire* subgraph of inputs which our subscription is composed of:
70+
Generally, `*ratom-context*` only has value during the evaluation of a component function (i.e., at "render time").
71+
When `*ratom-context*` has no value, reactive atoms behave differently.
72+
73+
You can simply call [`reagent.ratom/reactive?`](http://reagent-project.github.io/docs/master/reagent.ratom.html#var-reactive.3F)
74+
to find out whether your code is running in a reactive context.
75+
76+
#### Reactive context in re-frame
77+
78+
Now, here's where re-frame enters the picture:
79+
80+
- An **event handler** is a pure function, with no reactive context (it has an [interceptor](/re-frame/Interceptors) context).
81+
- A **subscription**, on the other hand, is a reactive atom (with *no* interceptor context).
82+
- Calling `subscribe` has the side-effect of *creating* a **subscription**.
83+
84+
Outside of a component function, a subscription's behavior differs:
85+
Not only the behavior of the reactive atom, but also the behavior of re-frame's subscription [caching](#caching) mechanism.
86+
87+
#### What this means for your app
88+
89+
Subscriptions and event handlers differ in purity and runtime context.
90+
This means they have a [coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/).
91+
92+
We [express some business logic with subscriptions](https://github.com/day8/re-frame/issues/753), and some with events.
93+
This introduces the coloring problem to our business domain.
94+
95+
We can ignore the problem in [some cases](https://github.com/day8/re-frame/issues/740#issuecomment-955749230),
96+
but the essential consequence of calling `subscribe` in an event handler is an unsafe cache.
97+
Calling `subscribe` allocates physical memory on the client, and re-frame has no way to deallocate it.
98+
This puts us back in C territory.
99+
100+
Thus, to safely get a value for `num-balloons-to-fill-kitchen`, we have to duplicate the business logic that we wrote into our subscription,
101+
along with the *entire* subgraph of subscription inputs:
67102

68103
<div class="cm-doc" data-cm-doc-no-eval data-cm-doc-no-edit data-cm-doc-no-result data-cm-doc-no-eval-on-init>
69104
(rf/reg-event-fx
@@ -72,7 +107,7 @@ That means, to get a usable value for `num-balloons-to-fill-kitchen`, we have to
72107
(let [kitchen-area (get-in db [:kitchen :area])
73108
kitchen-height (get-in db [:kitchen :height])
74109
kitchen-volume (* area height) ;; eyelids start drooping here
75-
std-balloon-volume 2.5
110+
std-balloon-volume 2.5
76111
num-balloons (/ kitchen-volume std-balloon-volume)
77112
num-bags-to-buy (js/Math.ceil
78113
(/ num-baloons balloons-per-bag))]
@@ -88,33 +123,37 @@ We sympathize with you developers, for the hours you may have spent poring over
88123

89124
### Caching
90125

91-
Subscriptions have a hidden caching mechanism, which stores the value as long as there is a component in the render tree which uses it.
92-
Basically, when components call `subscribe` with a particular `query-v`, re-frame sets up a callback.
93-
When those components unmount, this callback deletes the stored value.
94-
It removes the subscription from the graph, so that it will no longer recalculate.
126+
Subscriptions have a hidden caching mechanism, which stores the value as long as there is a component in the render tree which uses it.
127+
Basically, when components call `subscribe` with a particular `query-v`, re-frame sets up a callback.
128+
When those components unmount, this callback deletes the stored value.
129+
It removes the subscription from the graph, so that it will no longer recalculate.
95130
This is a form of [reference counting](https://en.wikipedia.org/wiki/Reference_counting) - once the last subscribing component unmounts, then the subscription is freed.
96131

97-
This often works as intended, and nothing gets in our way.
98-
It's elegant in a sense - a view requires certain values, and those values only matter when the view exists. And vice versa.
99-
But when these values are expensive to produce or store, their existence starts to matter.
100-
The fact that some view is creating and destroying them starts to seem arbitrary.
132+
This often works as intended, and nothing gets in our way.
133+
It's elegant in a sense - a view requires certain values, and those values only matter when the view exists. And vice versa.
134+
But when these values are expensive to produce or store, their existence starts to matter.
135+
The fact that some view is creating and destroying them starts to seem arbitrary.
101136
Subscriptions don't *need* to couple their behavior with that of their calling components.
102137

103-
The easy, automatic lifecycle behavior of subscriptions comes with a coupling of concerns. You can't directly control this lifecycle.
138+
The easy, automatic lifecycle behavior of subscriptions comes with a coupling of concerns. You can't directly control this lifecycle.
104139
You have to contol it by proxy, by mounting and unmounting your views. You can't *think* about your signal graph without thinking about views first.
105140

106-
The `app-db` represents your business state, and signals represent outcomes of your business logic. Views are just window dressing.
141+
The `app-db` represents your business state, and signals represent outcomes of your business logic. Views are just window dressing.
107142
We're tired of designing our whole business to change every time we wash the windows!
108143

109144
### Paths
110145

111-
A [layer-2](/re-frame/subscriptions/#the-four-layers) subscription basically *names* an `app-db` path. What does a layer-3 subscription *name*?
146+
A [layer-2](/re-frame/subscriptions/#the-four-layers) subscription basically *names* an `app-db` path.
147+
What does a layer-3 subscription *name*?
112148

113-
A materialized view, or a derived value.
149+
A materialized view of data, or a derived value.
114150

115-
Subscriptions occupy their own semantic domain, separate from `app-db`. Only within view functions (and other subscriptions) can we access this domain. Outside of views, they form an impenetrable blob.
151+
Subscriptions occupy their own semantic territory, separate from `app-db`.
152+
Only within view functions (and other subscriptions) can we access this domain.
153+
Outside of views, they form an impenetrable blob.
116154

117-
So, re-frame is simple. `app-db` represents and *names* the state of your app. Except, so does this network of subscription names. But you can't really *use* those, so just forget about it.
155+
So, re-frame is simple. `app-db` represents and *names* the state of your app.
156+
Except, so does this network of subscription names. But you can't always *use* those, only sometimes.
118157

119158
### Statefulness
120159

@@ -165,18 +204,24 @@ Why not simply have *everything* derive from `app-db`?
165204

166205
### A better way
167206

168-
Here's the good news about flows:
207+
Here's the good news about [flows](/re-frame/Flows):
169208

170-
__You can access a flow's output value any time, anywhere,__ since flows are controlled by re-frame/interceptors, not reagent/reactions.
209+
__You can access a flow's output value any time, anywhere,__
210+
since flows are controlled by re-frame/interceptors, not reagent/reactions.
171211
Instead of thinking about reactive context, just think about the outcome of the latest event.
212+
If you know `app-db`, you know your flow value.
213+
You can also [subscribe to flows](/re-frame/Flows/#subscribing-to-flows).
172214

173-
__If you know a flow's name, you know its output location,__ since flows store their output in `app-db`, at a static path.
174-
It doesn't matter how many other flows that flow depends on. The correct value simply stays where you put it.
215+
__If you know a flow's name, you know its location,__
216+
since flows store their output in `app-db`, at a static path.
217+
It doesn't matter what other flows & paths it depends on.
218+
The value you need simply stays where you put it.
175219

176-
__A flow's lifecycle is a pure function of `app-db`__.
177-
That means you explicitly define when a flow lives, dies, is registered or cleared. You do this directly, not via your component tree.
220+
__A flow's lifecycle is a pure function of `app-db`__.
221+
That means you explicitly define when a flow lives, dies, is registered or cleared.
222+
You do this directly, not via your component tree.
178223

179-
Like many Clojure patterns, flows are *both* nested *and* flat.
224+
Like many Clojure patterns, flows are *both* nested *and* flat.
180225
Even though `::num-balloons-to-fill-kitchen` depends on other flows, we can access it directly:
181226

182227
<div class="cm-doc" data-cm-doc-no-edit data-cm-doc-no-result data-cm-doc-no-eval-on-init>
@@ -198,7 +243,7 @@ Even though `::num-balloons-to-fill-kitchen` depends on other flows, we can acce
198243
(rf/reg-event-fx
199244
::order-ballons-for-kitchen-prank
200245
(fn [{:keys [balloons-per-bag] :as cofx} _]
201-
(let [num-balloons (rf/flow-output db ::num-balloons-to-fill-kitchen) ;; easy!
246+
(let [num-balloons (rf/get-flow db ::num-balloons-to-fill-kitchen) ;; easy!
202247
num-bags-to-buy (js/Math.ceil
203248
(/ num-balloons
204249
balloons-per-bag))]

src/re_frame/flow/alpha.cljc

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
(swap! flows empty))
100100
([id]
101101
(when-let [flow (lookup id)]
102-
(swap! flows dissoc flow)
102+
(swap! flows dissoc id)
103103
(swap! flows vary-meta update ::cleared assoc (:id flow) flow))))
104104

105105
(defn flow<- [id] {::flow<- id})

0 commit comments

Comments
 (0)