Skip to content

Commit

Permalink
[docs] Polish & update
Browse files Browse the repository at this point in the history
  • Loading branch information
kimo-k committed Nov 21, 2023
1 parent 3a1b703 commit 08e0c32
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 83 deletions.
94 changes: 44 additions & 50 deletions docs/Flows.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ We think flows offer a [Better Way](/re-frame/flows-advanced-topics#a-better-way
!!! Note "The DataFlow Paradigm"
Dataflow programming emerged in the 1970s, so it is almost as foundational as functional programming.
Indeed, reactive programming - so much the rage these days - is simply a subset of dataflow programming.
In contrast with imperative building blocks like 'if/then', 'next' and 'goto',
In contrast with imperative building blocks like `if/then`, `next` and `goto`,
dataflow programming implements control flow via the propagation of change.
Both the functional and dataflow paradigms have profoundly influenced the design of re-frame.
Hence, `re-frame's` tagline: "derived data, flowing".
Expand Down Expand Up @@ -335,21 +335,7 @@ For this, we use a `:live?` function.
The quote above deals with phenomenal life, but you can also think of `:live?` as in a tv or internet broadcast.
Data flows, but only when the flow itself is live.

Here's another room area flow:

<div class="cm-doc" data-cm-doc-result-format="pass-fail">
(rf/reg-flow
{:id :kitchen-area
:inputs {:w [:kitchen :width]
:h [:kitchen :length]}
:output (fn [{:keys [w h]}] (* w h))
:path [:kitchen :area]
:live-inputs {:tab [:tab]}
:live? (fn [{:keys [tab]}]
(= tab :kitchen))})
</div>

A barebones tab picker, and something to show us the value of `app-db`:
Let's try it out. For example, here's a barebones tab picker, and something to show us the value of `app-db`:

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

### Live?

Here's a more advanced version of our kitchen calculator flow.
This replaces our first `:kitchen-area` flow, since it has the same `:id`:

Here's a more advanced version of our room calculator flow.

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.
<div class="cm-doc" data-cm-doc-result-format="pass-fail">
(rf/reg-flow
{:id :kitchen-area
:inputs {:w [:kitchen :width]
:h [:kitchen :length]}
:output (fn [{:keys [w h]}] (* w h))
:path [:kitchen :area]
:live-inputs {:tab [:tab]}
:live? (fn [{:keys [tab]}]
(= tab :kitchen))})
</div>

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

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.
Re-frame only calculates the `:output` when the `:live?` function returns a truthy value.
Otherwise, the flow is presumed dead.

Let's test it out:

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

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

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

### Lifecycle

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

| transition | action |
|---|---|
| From **live** to **live** | run `:output` |
| From **dead** to **live** | run `:init` and `:output` |
| From **live** to **live** | run `:output` (when `:inputs` have changed) |
| From **dead** to **live** | run `:output` |
| From **live** to **dead** | run `:cleanup` |
| From **dead** to **dead** | do nothing |

Basically, *living* flows get output, *dying* flows get cleaned up, *arising* flows get initiated and output.

And independently of all this, `:output` only runs when `:inputs` have changed value.
Basically, *arising* flows get output, *living* flows get output as needed, and *dying* flows get cleaned up.

### Cleanup

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

Here's another demonstration. Think of it as a stripped-down todomvc.
You can add and remove items in a list:

<div class="cm-doc">
(rf/reg-sub ::items :-> (comp reverse ::items))
(rf/reg-sub :items :-> (comp reverse :items))

(rf/reg-event-db
::add-item
(fn [db [_ id]] (update db ::items conj id)))
(fn [db [_ id]] (update db :items conj id)))

(rf/reg-event-db
::delete-item
(fn [db [_ id]] (update db ::items #(remove #{id} %))))
(fn [db [_ id]] (update db :items #(remove #{id} %))))

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

(defn items []
(into [:div] (map item) @(rf/subscribe [::items])))
(into [:div] (map item) @(rf/subscribe [:items])))

(defn controls []
(let [id (atom 0)]
(fn []
[:div
[:span {:style clickable
:on-click #(do (rf/dispatch [::add-item (inc @id)])
(swap! id inc))} "Add"] " "
[:span {:style clickable
:on-click #(do (rf/dispatch [::delete-item @id])
(swap! id dec))} "Delete"] " "])))
(let [id (or (apply max @(rf/subscribe [:items])) 0)]
[:div
[:span {:style clickable
:on-click #(rf/dispatch [::add-item (inc id)])}
"Add"] " "
[:span {:style clickable
:on-click #(rf/dispatch [::delete-item id])}
"Delete"] " "]))

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

<div class="cm-doc" data-cm-doc-result-format="pass-fail">
(defn error-state-flow [{:keys [min-items max-items] :as requirements}]
{:id ::error-state
:path [::error-state]
:inputs {:items [::items]
:tab (rf/flow<- :current-tab)}
{:id :error-state
:path [:error-state]
:inputs {:items [:items]}
:output (fn [{:keys [items]}]
(let [ct (count items)]
(cond
Expand All @@ -535,15 +529,15 @@ And register a flow that fits our base requirements:
</div>

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

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

<div class="cm-doc">

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

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

Not only does changing the inputs lead to new output, but so does changing the flow itself.
Let's test it out:
Expand Down
105 changes: 75 additions & 30 deletions docs/flows-advanced-topics.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,54 @@ Introducing yet another demo app! Turns out, we were measuring the kitchen to fi
:ct num-bags-to-buy}]]})))
</div>

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!
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!

### Reactive context

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.
Between subscriptions and events, there is a [coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/).
To know if a thing has changed, you have to remember what it was.
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)).
Remembering is a side-effect.

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

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

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:
Generally, `*ratom-context*` only has value during the evaluation of a component function (i.e., at "render time").
When `*ratom-context*` has no value, reactive atoms behave differently.

You can simply call [`reagent.ratom/reactive?`](http://reagent-project.github.io/docs/master/reagent.ratom.html#var-reactive.3F)
to find out whether your code is running in a reactive context.

#### Reactive context in re-frame

Now, here's where re-frame enters the picture:

- An **event handler** is a pure function, with no reactive context (it has an [interceptor](/re-frame/Interceptors) context).
- A **subscription**, on the other hand, is a reactive atom (with *no* interceptor context).
- Calling `subscribe` has the side-effect of *creating* a **subscription**.

Outside of a component function, a subscription's behavior differs:
Not only the behavior of the reactive atom, but also the behavior of re-frame's subscription [caching](#caching) mechanism.

#### What this means for your app

Subscriptions and event handlers differ in purity and runtime context.
This means they have a [coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/).

We [express some business logic with subscriptions](https://github.com/day8/re-frame/issues/753), and some with events.
This introduces the coloring problem to our business domain.

We can ignore the problem in [some cases](https://github.com/day8/re-frame/issues/740#issuecomment-955749230),
but the essential consequence of calling `subscribe` in an event handler is an unsafe cache.
Calling `subscribe` allocates physical memory on the client, and re-frame has no way to deallocate it.
This puts us back in C territory.

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,
along with the *entire* subgraph of subscription inputs:

<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>
(rf/reg-event-fx
Expand All @@ -72,7 +107,7 @@ That means, to get a usable value for `num-balloons-to-fill-kitchen`, we have to
(let [kitchen-area (get-in db [:kitchen :area])
kitchen-height (get-in db [:kitchen :height])
kitchen-volume (* area height) ;; eyelids start drooping here
std-balloon-volume 2.5
std-balloon-volume 2.5
num-balloons (/ kitchen-volume std-balloon-volume)
num-bags-to-buy (js/Math.ceil
(/ num-baloons balloons-per-bag))]
Expand All @@ -88,33 +123,37 @@ We sympathize with you developers, for the hours you may have spent poring over

### Caching

Subscriptions have a hidden caching mechanism, which stores the value as long as there is a component in the render tree which uses it.
Basically, when components call `subscribe` with a particular `query-v`, re-frame sets up a callback.
When those components unmount, this callback deletes the stored value.
It removes the subscription from the graph, so that it will no longer recalculate.
Subscriptions have a hidden caching mechanism, which stores the value as long as there is a component in the render tree which uses it.
Basically, when components call `subscribe` with a particular `query-v`, re-frame sets up a callback.
When those components unmount, this callback deletes the stored value.
It removes the subscription from the graph, so that it will no longer recalculate.
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.

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

The easy, automatic lifecycle behavior of subscriptions comes with a coupling of concerns. You can't directly control this lifecycle.
The easy, automatic lifecycle behavior of subscriptions comes with a coupling of concerns. You can't directly control this lifecycle.
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.

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

### Paths

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

A materialized view, or a derived value.
A materialized view of data, or a derived value.

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.
Subscriptions occupy their own semantic territory, separate from `app-db`.
Only within view functions (and other subscriptions) can we access this domain.
Outside of views, they form an impenetrable blob.

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.
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 always *use* those, only sometimes.

### Statefulness

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

### A better way

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

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

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

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

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

<div class="cm-doc" data-cm-doc-no-edit data-cm-doc-no-result data-cm-doc-no-eval-on-init>
Expand All @@ -198,7 +243,7 @@ Even though `::num-balloons-to-fill-kitchen` depends on other flows, we can acce
(rf/reg-event-fx
::order-ballons-for-kitchen-prank
(fn [{:keys [balloons-per-bag] :as cofx} _]
(let [num-balloons (rf/flow-output db ::num-balloons-to-fill-kitchen) ;; easy!
(let [num-balloons (rf/get-flow db ::num-balloons-to-fill-kitchen) ;; easy!
num-bags-to-buy (js/Math.ceil
(/ num-balloons
balloons-per-bag))]
Expand Down
4 changes: 2 additions & 2 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ nav:
- Interceptors.md
- Effects.md
- Coeffects.md
- Flows.md
- Review TodoMVC: review-todomvc.md
- "INTERMEDIATE 4,5 & 6":
- Correcting a wrong: correcting-a-wrong.md
Expand All @@ -59,6 +60,7 @@ nav:
- "Interconnections": interconnections.md
- "Historical": historical.md
- MORE ADVANCED:
- Flows - advanced topics: flows-advanced-topics.md
- Stable Dom Handlers: on-stable-dom-handlers.md
- Browser Dynamics: browser-dynamics.md
- Reusable Components: reusable-components.md
Expand All @@ -72,8 +74,6 @@ nav:
- Solve the CPU hog problem: Solve-the-CPU-hog-problem.md
- Using Stateful JS Components: Using-Stateful-JS-Components.md
- The Logo Backstory: The-re-frame-logo.md
- Flows.md
- Flows - advanced topics: flows-advanced-topics.md
- FAQs:
- "How can I Inspect app-db?": FAQs/Inspecting-app-db.md
- "What Is Best Practice?": FAQs/BestPractice.md
Expand Down
2 changes: 1 addition & 1 deletion src/re_frame/flow/alpha.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
(swap! flows empty))
([id]
(when-let [flow (lookup id)]
(swap! flows dissoc flow)
(swap! flows dissoc id)
(swap! flows vary-meta update ::cleared assoc (:id flow) flow))))

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

0 comments on commit 08e0c32

Please sign in to comment.