This commit is contained in:
Mike Thompson 2016-12-05 07:34:20 +11:00
parent 54f39c8ce5
commit 3972b49555
4 changed files with 133 additions and 130 deletions

View File

@ -95,7 +95,7 @@ until we are back at the beginning of the loop. Each iteration is the same casca
Here are the 6 dominoes ...
### 1st Domino
### 1st Domino - Event Initiation
An `event` is sent when something happens - the user
clicks a button, or a websocket receives a new message.
@ -106,7 +106,7 @@ loop iteration after loop iteration, from one state to the next.
re-frame is `event` driven.
### 2nd Domino
### 2nd Domino - Event Handling
In response to an `event`, an application must compute
its implication (the ambition, the intent). This is known as `event handling`.
@ -119,9 +119,9 @@ Much of the time, only the "app state" of the SPA itself need
change, but sometimes the outside world must also be effected
(localstore, cookies, databases, emails, logs, etc).
### 3rd Domino
### 3rd Domino - Effect Handling
These descriptions of `effects` are actioned. The intent is realised.
These descriptions of `effects` are realised (actioned).
Now, to a functional programmer, `effects` are scary in a
[xenomorph kind of way](https://www.google.com.au/search?q=xenomorph).
@ -133,13 +133,13 @@ never achieving anything.
So re-frame embraces the protagonist nature of `effects` - the entire, unruly zoo of them - but
it does so in a controlled, debuggable, auditable, mockable, plugable way.
### Then what happens?
### A Pivot Point
So, that 3rd domino just changed the world and, very often,
The world has just changed and, very often,
one particular part of the world, namely the **app's state**.
re-frame's `app state` is held in one place - think of it like you
would an in-memory, central database for the app.
would an in-memory, central database for the app. (Much more details later)
When domino 3 changes this `app state`, it triggers the next part of the cascade
involving dominoes 4-5-6.
@ -149,7 +149,7 @@ involving dominoes 4-5-6.
The 4-5-6 domino cascade implements the formula made famous by Facebook's ground-breaking React library:
`v = f(s)`
A view `v` is a function `f` of the app state `s`.
A view, `v`, is a fun,ction, `f`, of the app state, `s`.
Or, said another way, there are functions `f` which compute what DOM nodes, `v`,
should be displayed to the user when the application is in a given app state, `s`.
@ -157,8 +157,8 @@ should be displayed to the user when the application is in a given app state, `s
Or, another way: **over time**, as `s` changes, `f`
will be re-run each time to compute new `v`, forever keeping `v` up to date with the current `s`.
Now, in our case, it is domino 3 which changes `s`, the application state,
and, in response, dominoes 4-5-6 are about re-running `f` to compute the new `v`
In our case, domino 3 changes `s`, the application state,
and, in response, dominoes 4-5-6 are concerned with re-running `f` to compute the new `v`
shown to the user.
Except, of course, there's nuances. For instance, there's no single `f` to run.
@ -167,10 +167,10 @@ and only part of `s` may change at any one time, so only part of the
`v` (DOM) need be re-computed and updated. And some parts of `v` might not
even be showing right now.
### Domino 4
### Domino 4 - Query
Domino 4 is about extracting data from "app state". The right data,
in the right format, for view functions (Domino 5).
in the right format, for view functions (which are Domino 5).
Domino 4 is a novel and efficient de-duplicated signal graph which
runs query functions on the app state, `s`, efficiently computing
@ -179,22 +179,23 @@ reactive, multi-layered, "materialised views" of `s`.
(Relax about any unfamiliar terminology, you'll soon
see how simple the code actually is)
### Domino 5
### Domino 5 - View
Domino 5 is one or more **view functions** (aka Reagent components) which compute what
UI DOM should be displayed for the user.
They take data, delivered reactively by the queries of domino 4,
and compute hiccup-formatted data, which is a description of the DOM required.
To render the right UI, they need to source application state, which is
delivered reactively via the queries of Domino 4 .They
compute hiccup-formatted data, which is a description of the DOM required.
### Domino 6
### Domino 6 - DOM
Domino 6 is not something you need write yourself - instead it is handled for you
You don't write Domino 6 - it is handled for you
by Reagent/Rect. I mention it here
for completeness and to fully close the loop.
This is the step in which the hiccup-formatted
"descriptions of required DOM", returned by Domino 5, are made real. The
"descriptions of required DOM", returned by the view functions of Domino 5, are made real. The
browser DOM nodes are mutated.
## A Cascade Of Simple Functions
@ -206,7 +207,9 @@ tested independently. They take data, transform it and return new data.
The loop itself is very mechanical in operation.
So, there's a regularity, simplicity and
certainty to how a re-frame app goes about its business,
which leads, in turn, to an ease in reasoning and debugging.
which leads, in turn, to an ease in reasoning and debugging. This is
key to why re-frame is so pleasing to work with - it is just so
straightforward.
## Managing mutation
@ -221,13 +224,11 @@ after those dominoes.
## Code Fragments
Let's now understand this
domino narrative in terms of code fragments.
> You shouldn't expect
to completely grok the code presented below. We're still in overview mode, getting
the 30,000 foot view. There are later tutorials for the details.
Let's take this domino narrative one step further and introduce some code fragments.
> Don't expect
to completely grok the terse code presented below. We're still at 30,000 feet. Details later.
**Imagine:** the UI of an SPA shows a list of items. This user
clicks the "delete" button next to the 3rd item in a list.
@ -242,7 +243,9 @@ like this:
#(re-frame.core/dispatch [:delete-item 2486])
```
`dispatch` is the means by which you emit an `event`. An `event` is a vector and, in this case,
`dispatch` emits an `event`.
An re-frame `event` is a vector and, in this case,
it has 2 elements: `[:delete-item 2486]`. The first element,
`:delete-item`, is the kind of event. The `rest` is optional, further data about the
`event` - in this case, my made-up id, `2486`, for the item to delete.
@ -263,25 +266,24 @@ might look like:
{:db (dissoc-in db [:items item-id])})) ;; effect is change db
```
On program starup, this event handler (function) `h` would have been
associated with `:delete-item` `events` in this way:
On program startup, `h` would have been
associated with `:delete-item` `events` like this:
```clj
(re-frame.core/reg-event-fx :delete-item h)
```
### Code For Domino 3
An `effect handler` (function) actions the `effect` returned by the call to `h`.
That `effect` was the map:
An `effect handler` (function) actions the `effects` returned by `h`:
```clj
{:db (dissoc-in db [:items item-id])}
```
Keys in this map identify the required `effect`, and the values of the map
So that's a map. The keys identify the required kind of `effect`, and the values
supplying further details.
A key of `:db` means to update the app state, with the new computed value.
The update of "app state", which re-frame manages for you,
This update of "app state", which re-frame manages for you,
is a mutative step, facilitated by re-frame itself
when it sees a `:db` effect.
@ -303,7 +305,7 @@ subscription acts more like an accessor.
(:items db)) ;; not much of a materialised view
```
On program startup, such a query-fn must be registered,
On program startup, such a query-fn must associated with a key,
(for reasons obvious in the next domino) like this:
```clj
(re-frame.core/reg-sub :query-items query-fn)
@ -312,10 +314,11 @@ On program startup, such a query-fn must be registered,
### Code For Domino 5
Because the query function re-computed a new value, a view (function) which subscribes
to "items", is called automatically (reactively) to re-compute DOM. It produces
to "items", is called automatically (reactively) to re-compute DOM.
It produces
a hiccup-formatted data structure describing the DOM nodes required (no DOM nodes
for the deleted item, obviously, but otherwise the same DOM as last time).
```clj
(defn items-view
[]
@ -323,8 +326,8 @@ for the deleted item, obviously, but otherwise the same DOM as last time).
[div: (map item-render @items])) ;; assume item-render already written
```
Notice how `items` is "sourced" from "app state". View function use `subscribe` with a key
originally used to register a query function.
Notice how `items` is "sourced" from "app state" via `subscribe`. It is called with a query key
to identify what data it needs, which should make a prior registration.
### Code For Domino 6
@ -332,7 +335,7 @@ The computed DOM (hiccup) is made real by Reagent/React. No code from you requir
The DOM "this
time" is the same as last time, except for the absence of DOM for the
deleted item.
deleted item, so the mutation will be to remove some DOM nodes.
### 3-4-5-6 Summary

View File

@ -280,8 +280,7 @@ of subscriptions and more explanation can be found in the todomvc example.
```
If, later, a view function subscribes to a query like this:
`(subscribe [:some-query-id])` ;; note use of `:some-query-id`
then `a-query-fn` will be used to perform the query over application state
and deliver a stream of values.
then `a-query-fn` will be used to perform the query over the application state.
Each time application state changes, `a-query-fn` will be
called again to compute a new materialised view (a new computation over app state)
@ -425,12 +424,12 @@ Django, Rails, Handlebars or Mustache -- they maps data to HTML -- except for tw
## Kick Starting The App
Below, `run` is the function called when the HTML loads. It kicks off the
application.
Below, `run` is the called when the HTML page has loaded
to kick off the application.
It has two tasks:
1. load the initial application state
2. "mount" the GUI on an existing DOM element. Causes an initial render.
2. "mount" the GUI onto an existing DOM element.
```clj
(defn ^:export run
@ -444,7 +443,8 @@ After `run` is called, the app passively waits for events.
Nothing happens without an `event`.
When it comes to establishing initial application state, you'll
notice the use of `dispatch-sync`, rather than `dispatch`. This ensures a correct
notice the use of `dispatch-sync`, rather than `dispatch`. This is something of
cheat which ensures a correct
structure exists in `app-db` before any subscriptions or event handlers run.
## Summary

View File

@ -44,9 +44,9 @@ In a similar spirit, you can almost see re-frame's 6 domino cascade like this:
(->> event ;; domino 1
event-handler ;; 2
effect-handler ;; 3
-- app state --- ;;
-- app state --- ;; pivot
queries ;; 4
view-fns ;; 5
views ;; 5
React) ;; 6
```
@ -59,25 +59,25 @@ this too much, re-frame looks
after it for you. It will thread (convey) data from one domino function to the next.
It will call your functions at the right time, with the right (data) arguments.
My second answer is: the method varies from domino to domino. Read on. But our
My second answer is: the method/transport varies from domino to domino. Read on. But our
main focus is on the reactive flow of 3-4-5-6.
## 1 -> 2
`dispatch` queues events and they are not immediately processed. So event handling is done async.
`dispatch` queues events and they are not immediately processed. Event handling is done async.
A router reads events from this queue, looks up the right handler and calls it.
xxx
A router reads events from this queue, looks up the associated handler and calls it.
## 2 -> 3
Except I lied in the previous section. The router doesn't really look up a single "handler". Instead it looks up an interceptor chain (described later).
That interceptor chain is a pipeline of functions. The last of them
xxx
Except I lied in the previous section. The router doesn't really look
up a single "handler". Instead it looks up an interceptor chain. The method by which
an Interceptor chain is executed is discussed in great detail shortly.
## 3->4->5->6
So now we are at the meat and potatoes. The real subject of this tutorial.
## On Flow
@ -100,3 +100,79 @@ your neck, read it again until it does, because it is important.
Steve Grand
### How Flow Happens In Reagent
To implement a reactive flow, Reagent provides a `ratom` and a `reaction`.
re-frame uses both of these
building blocks, so let's now make sure we understand them.
`ratoms` behave just like normal ClojureScript atoms. You can `swap!` and `reset!` them, `watch` them, etc.
From a ClojureScript perspective, the purpose of an atom is to hold mutable data. From a re-frame
perspective, we'll tweak that paradigm slightly and **view a `ratom` as having a value that
changes over time.** Seems like a subtle distinction, I know, but because of it, re-frame sees a
`ratom` as a Signal. [Pause and read this](http://elm-lang.org:1234/guide/reactivity).
The 2nd building block, `reaction`, acts a bit like a function. It's a macro which wraps some
`computation` (a block of code) and returns a `ratom` holding the result of that `computation`.
The magic thing about a `reaction` is that the `computation` it wraps will be automatically
re-run whenever 'its inputs' change, producing a new output (return) value.
Eh, how?
Well, the `computation` is just a block of code, and if that code dereferences one or
more `ratoms`, it will be automatically re-run (recomputing a new return value) whenever any
of these dereferenced `ratoms` change.
To put that yet another way, a `reaction` detects a `computation's` input Signals (aka input `ratoms`)
and it will `watch` them, and when, later, it detects a change in one of them, it will re-run that
computation, and it will `reset!` the new result of that computation into the `ratom` originally returned.
So, the `ratom` returned by a `reaction` is itself a Signal. Its value will change over time when
the `computation` is re-run.
So, via the interplay between `ratoms` and `reactions`, values 'flow' into computations and out
again, and then into further computations, etc. "Values" flow (propagate) through the Signal graph.
But this Signal graph must be without cycles, because cycles cause mayhem! re-frame achieves
a unidirectional flow.
Right, so that was a lot of words. Some code to clarify:
```Clojure
(ns example1
(:require-macros [reagent.ratom :refer [reaction]]) ;; reaction is a macro
(:require [reagent.core :as reagent]))
(def app-db (reagent/atom {:a 1})) ;; our root ratom (signal)
(def ratom2 (reaction {:b (:a @app-db)})) ;; reaction wraps a computation, returns a signal
(def ratom3 (reaction (condp = (:b @ratom2) ;; reaction wraps another computation
0 "World"
1 "Hello")))
;; Notice that both computations above involve de-referencing a ratom:
;; - app-db in one case
;; - ratom2 in the other
;; Notice that both reactions above return a ratom.
;; Those returned ratoms hold the (time varying) value of the computations.
(println @ratom2) ;; ==> {:b 1} ;; a computed result, involving @app-db
(println @ratom3) ;; ==> "Hello" ;; a computed result, involving @ratom2
(reset! app-db {:a 0}) ;; this change to app-db, triggers re-computation
;; of ratom2
;; which, in turn, causes a re-computation of ratom3
(println @ratom2) ;; ==> {:b 0} ;; ratom2 is result of {:b (:a @app-db)}
(println @ratom3) ;; ==> "World" ;; ratom3 is automatically updated too.
```
So, in FRP-ish terms, a `reaction` will produce a "stream" of values over time (it is a Signal),
accessible via the `ratom` it returns.
Okay, that was all important background information for what is to follow.

View File

@ -67,82 +67,6 @@ going further (certainly read the first two):
### How Flow Happens In Reagent
To implement FRP, Reagent provides a `ratom` and a `reaction`.
re-frame uses both of these
building blocks, so let's now make sure we understand them.
`ratoms` behave just like normal ClojureScript atoms. You can `swap!` and `reset!` them, `watch` them, etc.
From a ClojureScript perspective, the purpose of an atom is to hold mutable data. From a re-frame
perspective, we'll tweak that paradigm slightly and **view a `ratom` as having a value that
changes over time.** Seems like a subtle distinction, I know, but because of it, re-frame sees a
`ratom` as a Signal. [Pause and read this](http://elm-lang.org:1234/guide/reactivity).
The 2nd building block, `reaction`, acts a bit like a function. It's a macro which wraps some
`computation` (a block of code) and returns a `ratom` holding the result of that `computation`.
The magic thing about a `reaction` is that the `computation` it wraps will be automatically
re-run whenever 'its inputs' change, producing a new output (return) value.
Eh, how?
Well, the `computation` is just a block of code, and if that code dereferences one or
more `ratoms`, it will be automatically re-run (recomputing a new return value) whenever any
of these dereferenced `ratoms` change.
To put that yet another way, a `reaction` detects a `computation's` input Signals (aka input `ratoms`)
and it will `watch` them, and when, later, it detects a change in one of them, it will re-run that
computation, and it will `reset!` the new result of that computation into the `ratom` originally returned.
So, the `ratom` returned by a `reaction` is itself a Signal. Its value will change over time when
the `computation` is re-run.
So, via the interplay between `ratoms` and `reactions`, values 'flow' into computations and out
again, and then into further computations, etc. "Values" flow (propagate) through the Signal graph.
But this Signal graph must be without cycles, because cycles cause mayhem! re-frame achieves
a unidirectional flow.
Right, so that was a lot of words. Some code to clarify:
```Clojure
(ns example1
(:require-macros [reagent.ratom :refer [reaction]]) ;; reaction is a macro
(:require [reagent.core :as reagent]))
(def app-db (reagent/atom {:a 1})) ;; our root ratom (signal)
(def ratom2 (reaction {:b (:a @app-db)})) ;; reaction wraps a computation, returns a signal
(def ratom3 (reaction (condp = (:b @ratom2) ;; reaction wraps another computation
0 "World"
1 "Hello")))
;; Notice that both computations above involve de-referencing a ratom:
;; - app-db in one case
;; - ratom2 in the other
;; Notice that both reactions above return a ratom.
;; Those returned ratoms hold the (time varying) value of the computations.
(println @ratom2) ;; ==> {:b 1} ;; a computed result, involving @app-db
(println @ratom3) ;; ==> "Hello" ;; a computed result, involving @ratom2
(reset! app-db {:a 0}) ;; this change to app-db, triggers re-computation
;; of ratom2
;; which, in turn, causes a re-computation of ratom3
(println @ratom2) ;; ==> {:b 0} ;; ratom2 is result of {:b (:a @app-db)}
(println @ratom3) ;; ==> "World" ;; ratom3 is automatically updated too.
```
So, in FRP-ish terms, a `reaction` will produce a "stream" of values over time (it is a Signal),
accessible via the `ratom` it returns.
Okay, that was all important background information for what is to follow. Back to the diagram ...
## Components