You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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`:
353
339
354
340
<divclass="cm-doc">
355
341
(def tabs [:kitchen:garage])
@@ -382,15 +368,25 @@ A barebones tab picker, and something to show us the value of `app-db`:
382
368
383
369
### Live?
384
370
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.
388
372
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.
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`.
392
387
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.
394
390
395
391
Let's test it out:
396
392
@@ -410,7 +406,8 @@ Let's test it out:
410
406
411
407
<divid="tabbed-app"></div>
412
408
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?
414
411
415
412
### Lifecycle
416
413
@@ -419,14 +416,12 @@ Depending on the return value of `:live?`, re-frame handles one of 4 possible st
419
416
420
417
| transition | action |
421
418
|---|---|
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`|
424
421
| From **live** to **dead**| run `:cleanup`|
425
422
| From **dead** to **dead**| do nothing |
426
423
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.
430
425
431
426
### Cleanup
432
427
@@ -457,32 +452,32 @@ Not only do flows have a lifecycle (defined by `:live?`, `:init` and `:cleanup`)
457
452
458
453
Here's another demonstration. Think of it as a stripped-down todomvc.
459
454
You can add and remove items in a list:
455
+
460
456
<divclass="cm-doc">
461
-
(rf/reg-sub ::items :-> (comp reverse ::items))
457
+
(rf/reg-sub :items :-> (comp reverse :items))
462
458
463
459
(rf/reg-event-db
464
460
::add-item
465
-
(fn [db [_ id]] (update db ::items conj id)))
461
+
(fn [db [_ id]] (update db :items conj id)))
466
462
467
463
(rf/reg-event-db
468
464
::delete-item
469
-
(fn [db [_ id]] (update db ::items #(remove #{id} %))))
465
+
(fn [db [_ id]] (update db :items #(remove #{id} %))))
Copy file name to clipboardexpand all lines: docs/flows-advanced-topics.md
+75-30
Original file line number
Diff line number
Diff line change
@@ -51,19 +51,54 @@ Introducing yet another demo app! Turns out, we were measuring the kitchen to fi
51
51
:ct num-bags-to-buy}]]})))
52
52
</div>
53
53
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!
55
57
56
58
### Reactive context
57
59
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.
60
63
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.
63
66
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).
65
69
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:
@@ -88,33 +123,37 @@ We sympathize with you developers, for the hours you may have spent poring over
88
123
89
124
### Caching
90
125
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.
95
130
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.
96
131
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.
101
136
Subscriptions don't *need* to couple their behavior with that of their calling components.
102
137
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.
104
139
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.
105
140
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.
107
142
We're tired of designing our whole business to change every time we wash the windows!
108
143
109
144
### Paths
110
145
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*?
112
148
113
-
A materialized view, or a derived value.
149
+
A materialized view of data, or a derived value.
114
150
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.
116
154
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.
118
157
119
158
### Statefulness
120
159
@@ -165,18 +204,24 @@ Why not simply have *everything* derive from `app-db`?
165
204
166
205
### A better way
167
206
168
-
Here's the good news about flows:
207
+
Here's the good news about [flows](/re-frame/Flows):
169
208
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.
171
211
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).
172
214
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.
175
219
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.
178
223
179
-
Like many Clojure patterns, flows are *both* nested *and* flat.
224
+
Like many Clojure patterns, flows are *both* nested *and* flat.
180
225
Even though `::num-balloons-to-fill-kitchen` depends on other flows, we can access it directly:
0 commit comments