Skip to content

Commit

Permalink
[docs] Basic calculator flow
Browse files Browse the repository at this point in the history
  • Loading branch information
kimo-k committed Oct 26, 2023
1 parent 33f19d1 commit 9b5ae6f
Showing 1 changed file with 126 additions and 23 deletions.
149 changes: 126 additions & 23 deletions docs/Flows.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,130 @@
> This feature is proposed for re-frame. But it isn't available yet.
## Flows
## The story so far

re-frame's process:
1. Users cause Events.
2. Events cause Effects
3. Effects cause State Changes
4. State Changes cause View rerendering.
1. **Users** cause **Events**
2. **Events** cause **Effects**
3. **Effects** cause **State Changes**
4. **State Changes** cause **View rerendering**

We're about to add a new capability to step 3.
We're about to add a new capability to step 3. Let's begin:

## Flows
<div class="cm-doc">
(ns re-frame.example.flows
(:require [re-frame.alpha :as rf]
[reagent.dom :as rdom]))
</div>

## Flows

A `flow` calculates a derived value "automatically".

<div class="cm-doc" data-cm-doc-result-format="pass-fail">
(def my-flow
{:id :kitchen-area
:inputs {:w [:kitchen :width]
:h [:kitchen :height]}
:output (fn area [previous-area {:keys [w h] :as inputs}]
(* w h))
:path [:kitchen :area]})
</div>

- A `flow` has `:inputs`, which are paths in `app-db`.
- When the `:inputs` change, it evaluates a new `:output` value.
- Then, it puts the `:output` value back into `app-db`, at a specific `:path`.

## A Basic use-case

Let's try using our flow in an app (see the basic [app demo](dominoes-live.md) if this is unfamiliar):

<div class="cm-doc">
(rf/reg-sub :width (fn [db _] (get-in db [:kitchen :width])))
(rf/reg-sub :height (fn [db _] (get-in db [:kitchen :height])))
(rf/reg-event-db :width (fn [db [_ w]] (assoc-in db [:kitchen :width] w)))
(rf/reg-event-db :height (fn [db [_ h]] (assoc-in db [:kitchen :height] h)))
(rf/reg-event-db :init (fn [db _] {:kitchen {:width 20 :height 20}}))

(defn get-text [e] (-> e .-target .-value))

(defn dimension-fields []
[:form
[:h4 "Kitchen Calculator"] [:strong "width: "]
[:input {:type "number" :label "width"
:value @(rf/subscribe [:width])
:on-change #(rf/dispatch [:width (get-text %)])}]
[:br] [:strong "height: "]
[:input {:type "number" :label "height"
:value @(rf/subscribe [:height])
:on-change #(rf/dispatch [:height (get-text %)])}]])
</div>

Here comes the interesting part. We register our flow within the app:

<div class="cm-doc" data-cm-doc-result-format="pass-fail">
(rf/reg-flow my-flow)
</div>

We subscribe to a global `:flow` key, which can fetch the value for any flow (see XXX-sub-alpha if this is unfamiliar). This simply does a `(get-in app-db (:path my-flow))`:

`Flows` re-calculate derived values "automatically".
<div class="cm-doc">
(def kitchen-area (rf/sub :flow {:id :kitchen-area}))
</div>

And we use this value to render our final component:

<div class="cm-doc">
(defn room-calculator []
[:div {:style {:padding "1rem"
:border "2px solid grey"}}
[dimension-fields]
" Area: "
@kitchen-area])

(rf/dispatch-sync [:init])
(rdom/render [room-calculator] (js/document.getElementById "room-calculator"))
</div>

A `Flow` "observes" N paths within `app-db`, and if any of them changes,
then that `flow` will compute a new derivative value and store that value
at another path within `app-db`.
<div id="room-calculator"></div>

Error state is often derivative, so we'll now use that as our animating example.


Isn't that remarkable! What, you say it's *unremarkable?* Well, that's even better.

## Remarks

Here's why this basic flow might not excite you:

#### Can't I just use events?

> Re-frame can already set values. Events were the one true way to update `app-db`. Why invent _another_ mechanism for this?
In this sense, they are redundant. Rather than use a flow, you could simply call a `derive-area` within each relevant event:

<div class="cm-doc" data-cm-doc-no-eval data-cm-doc-no-edit data-cm-doc-no-result>
(defn derive-area [{:keys [width height] :as room}]
(assoc room :area (* width height)))

(rf/reg-event-db
:width
(fn [db [_ w]]
(-> db
(assoc-in [:kitchen :width] w)
(update :kitchen derive-area))))
</div>

This works just fine... or does it? Actually, we forgot to change the `:height` event. Our area calculation will be wrong every time the user changes the height! Once they change the width it will be right again. Easy to fix, but the point is that we had to fix it at all. How many events do we need to review? In a mature app, this isn't a trivial question.

*Design is all tradeoffs*. Flows allow us to say "This value simply derives from these inputs. It simply changes whenever they do." We do this at the expense of some "spooky action at a distance" - in other words, we accept that no particular event will be responsible for that change.

#### Are flows just reactions, or cursors?

You might notice a similarity with [reagent.core/cursor](https://reagent-project.github.io/docs/master/reagent.core.html#var-cursor).

Both offer ways to react to *part of* a value (such as a subtree within a map). Reagent controls *when* a cursor updates, presumably during the evaluation of a component function. Flows, on the other hand, are controlled by re-frame, running every time an `event` occurs.

Cursors use a single path, whether reading with `deref`, or writing with `reset!` or `swap!`. With flows, you declare several input paths, and a separate output path. You don't `reset!` a `flow`. Instead, this just happens each `event`, if the inputs have changed.

With flows, you can implement business logic as a reactive state machine, fully independent from Reagent & React. This has some deep implications - see XXX-flow-rationale for a full explanation.

## Imaginary Example

Expand All @@ -29,9 +135,6 @@ disable the "Continue" button. And, if the vector has more than 5 elements, the
show a warning "Only the first 5 items will be used". Yeah, I know, all very contrived.

<div class="cm-doc">
(ns re-frame.flow-demo
(:require [reagent.dom :as rdom]
[re-frame.alpha :as rf]))

(rf/reg-sub ::items :-> (comp reverse ::items))

Expand All @@ -44,10 +147,11 @@ show a warning "Only the first 5 items will be used". Yeah, I know, all very co
@(rf/subscribe [::items])))
</div>

Further, imagine that there are three different events (dispatched because of user button clicks) that change the state of the vector:
- `::clear-all`
- `::add-item`
- `::delete-item`
#### Fixing the db

It's common to design apps which prepare certain db paths when a high-level state changes, such as when switching tabs. With flows, this preparation is an official library feature. Instead of writing custom events, you can use the `:cleanup` and `:init` keys and your colleages will know exactly what you're doing.



<div class="cm-doc">
(rf/reg-event-db
Expand Down Expand Up @@ -110,8 +214,7 @@ that update the vector. While possible, it would be fragile WRT to certain chang
someone added a fourth event handler that changed the vector, but they forgot
to calculate the error state?

Having said that, design is all tradeoffs. Using a flow to automate the recalculations might be more resilient to changes, but it is less explicit. Such
automation can make code more abstract, perhaps to the point where it causes "spooky action at a distance" (it gets hard to figure out what
Having said that, design is all tradeoffs. Using a flow to automate the recalculations might be more resilient to changes, but it is less explicit. Such automation can make code more abstract, perhaps to the point where it causes "spooky action at a distance" (it gets hard to figure out what
is causing specific changes).

## How?
Expand All @@ -124,7 +227,7 @@ To register a flow, you have to supply 3 things:

You call the `reg-flow` with a map that provides the flow specification.
```
(re-flow
(reg-flow
{:value {
:inputs {:v [:some :path :into :app-db]}
Expand Down

0 comments on commit 9b5ae6f

Please sign in to comment.