mirror of
https://github.com/status-im/re-frame.git
synced 2025-02-23 15:28:09 +00:00
WIP
This commit is contained in:
parent
54f39c8ce5
commit
3972b49555
79
README.md
79
README.md
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user