Readme WIP

This commit is contained in:
mike-thompson-day8 2015-01-21 09:40:49 +11:00
parent 789ade4597
commit adf9ab52a3
1 changed files with 159 additions and 102 deletions

261
README.md
View File

@ -9,13 +9,13 @@ Todo:
re-frame is a tiny [Reagent] framework for writing [SPAs] using ClojureScript.
This document proposes a **pattern** for structuring an SPA using ClojureScript and Reagent, and the repo provides a **reference implementation**.
This repo has documents which propose a **pattern** for structuring an SPA using ClojureScript and Reagent. The repo also contains a **reference implementation**.
re-frame isn't an MVC framework. Instead, it is a functional RACES framework - Reactive-Atom Component Event Subscription (I love the smell of acronym in the morning).
re-frame isn't an MVC framework. Instead, it is a functional RACES framework - Reactive-Atom Component Event Subscription framework (I love the smell of acronym in the morning).
### Overview
The re-frame pattern is simple. So simple, in fact, that the reference implementation in this repo is barely 100 lines of code. Compare that to Ember or Angular which weigh in at 10,000s lines of code (I'm cheating ... to be fair, I should include the few hundred lines in Reagent too, but you get the idea)
The re-frame pattern is simple. So simple, in fact, that the reference implementation in this repo is barely 100 lines of code. That compares to 10s of thousands in Angular or Ember. (And, yes, I'm cheating ... to be fair, I should include the few hundred lines in Reagent too, but you get the idea.)
To build an app using re-frame, you:
- design your app's data structure (data layer)
@ -26,15 +26,15 @@ To build an app using re-frame, you:
All the functions you write are pure, so the pieces of your app can be
described, understood and tested independently.
Despite its simplicity, re-frame is impressively buzzword compliant: it has FRP-nature, unidirectional data flow, pristinely pure functions, uses conveyor belts, statecharts and claims a hammock conception.
Despite its simplicity, re-frame is impressively buzzword compliant: it has FRP-nature, unidirectional data flow, pristinely pure functions, conveyor belts, statecharts and claims an immaculate hammock conception.
### Client Side Bias
We write larger, more complicated SPAs which have a Parisian's indifference for servers.
Unsurprising, re-frame's design reflects our needs. So there's nothing in re-frame about, say, routes, or sessions or accessing server-side services, etc. It is just about writing browser-based apps which are almost unaware they are in a browser. That doesn't mean re-frame wouldn't work well when a server is heavily involved, its just that we haven't tweaked it in that direction.
Unsurprising, re-frame's design reflects our needs. So there's nothing in re-frame about, say, routes, or sessions or syncing client state with server state, etc. It is just about writing browser-based apps which are desktop-like. That doesn't mean re-frame wouldn't work well when servers are more centrally involved, its just that we haven't tweaked it in that direction.
Remember, re-frame is more of a pattern than an implementation, so you can easily fork it in whatever direction you need.
Remember, re-frame is more of a pattern than an implementation, so you can easily fork it in whatever direction you need.
At small scale, any framework seems like pesky overhead. The
explanatory examples in here are necessarily small scale, so you'll need to
@ -50,11 +50,11 @@ something original and clever).
First, above all we believe in the one true [Dan Holmsand], the creator of Reagent, and his divine instrument the `ratom`. We genuflect towards Sweden once a day.
Second, we believe in ClojureScript, and the process of building a system out of pure functions.
Second, we believe in ClojureScript, immutable data and the process of building a system out of pure functions.
Third, we believe that [FRP] is a honking great idea. You might be tempted to see
Reagent as simply another of the React wrappers - a sibling to [OM] and [quiescent](https://github.com/levand/quiescent). But you'll only really "get"
Reagent when you view it as an FRP library. To put that another way, we think
Reagent when you view it as an FRP-ish library. To put that another way, we think
that Reagent, at its best, is closer in
nature to [Hoplon] or [Elm] than it is OM.
@ -62,19 +62,25 @@ Finally, we believe in one-way data flow. No cycles! We don't like read/write `
uses two, separate, one-way flows to achieve it, and those two flows
are different in nature.
If you are new to FRP, I'd recommend both of these resources before going further:
- [this excellent video](http://www.infoq.com/presentations/ClojureScript-Javelin) by Alan Dopert
- [this FRP backgrounder](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754)
### FRP Clarity
In this document, when I talk about FRP, I'm being a bit loose with my terms. I should talk about re-frame using "discrete, asynchronous, push FRP" without "glitch free" guarantees.
Terminology the FRP space can get people hot under the collar, especially those who believe in continuous-time semantics. They'd dismissively claim that re-frame is only doing reactive, asynchronous, data flow programming, not true FRP. We sympathetically note their purist concerns, quip "close enough" and move on.
## The Parts
If you are new to FRP, I'd recommend browsing these resources before going further:
- [light FRP backgrounder](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754)
- [presentation (video)](http://www.infoq.com/presentations/ClojureScript-Javelin) by Alan Dipert (co-author of Hoplon)
- [2012 taxonomy and survey](http://soft.vub.ac.be/Publications/2012/vub-soft-tr-12-13.pdf)
- [serious pants thesis](https://www.seas.harvard.edu/sites/default/files/files/archived/Czaplicki.pdf)
## Explaining re-frame
To explain re-frame, I'll incrementally develop a diagram, explaining each part as it is added.
Along the way, I'll be using [reagent] at an intermediate to advanced level. But this is no introductory reagent tutorial and you will need to have done one of those before continuing here. Try
[Introductory Tutorial](http://reagent-project.github.io/) or
[this Reagent tutorial](https://github.com/jonase/reagent-tutorial) or
Along the way, I'll be using [Reagent] at an intermediate to advanced level. But this is no introductory reagent tutorial and you will need to have done one of those before continuing here. Try
[The Introductory Tutorial](http://reagent-project.github.io/) or
[this one](https://github.com/jonase/reagent-tutorial) or
[Building Single Page Apps with Reagent](http://yogthos.net/posts/2014-07-15-Building-Single-Page-Apps-with-Reagent.html).
##### On Data
@ -84,13 +90,13 @@ Along the way, I'll be using [reagent] at an intermediate to advanced level. Bu
##### The Big Ratom
Our re-frame diagram starts (very modestly) with Fogus' "well-formed data at rest" bit:
Our re-frame diagram starts (very modestly) with Fogus' ***well-formed data at rest*** bit:
```
app-db
```
re-frame says that you put your data into one place which we'll call `app-db`. Structure the data in that place, of course. And [give it a schema](https://github.com/miner/herbert).
re-frame says that you put your data into one place which we'll call `app-db`. Structure the data in that place, of course, and [give it a schema](https://github.com/Prismatic/schema).
Now, this advice is not the slightest bit controversial for 'real' databases, right?
You'd happily put all your well-formed data into PostgreSQL or MySQL. But within a running application (in memory), it is different. If you have a background in OO, this data-in-one-place business is a hard one to swallow. You've
@ -98,7 +104,7 @@ spent your life breaking systems into pieces, organised around behaviour and try
to hide the data. I still wake up in a sweat some nights thinking about all
that Clojure data lying around exposed and passive.
But, as Fogus tells us, data at rest is the easy bit.
But, as Fogus reminds us, data at rest is the easy bit.
From here on in this document, we'll assume `app-db` is one of these:
@ -110,7 +116,7 @@ Although it is a `Reagent atom` (hereafter `ratom`), I'd encourage you to think
It will contain structured data. You will need to query that data. You will perform CRUD
and other transformations on it. You'll often want to transact on this
database atomically, etc. So "in-memory database"
seems a more useful paradigm than plain old atom.
seems a more useful paradigm than plain old map-in-atom.
Finally, a clarification: `app-db` doesn't actually have to be a reagent/atom containing
a map. In theory, re-frame
@ -125,37 +131,39 @@ I'm going to quote verbatim from Elm's website:
2. Save and Undo become quite easy. Many applications would benefit from the ability to save all application state and send it off to the server so it can be reloaded at some later date. This is extremely difficult when your application state is spread all over the place and potentially tied to objects that cannot be serialized. With a central store, this becomes very simple. Many applications would also benefit from the ability to easily undo user's actions. For example, a painting app is better with Undo. Since everything is immutable in Elm, this is also very easy. Saving past states is trivial, and you will automatically get pretty good sharing guarantees to keep the size of the snapshots down.
##### Important Quote
[Hoplon] takes the same approach via what they called `stem cells`, whch is a root source of data.
From what I've seen, it is currently obligatory to include the following quote whenver talking about FRP. This rule may be relaxed in the future, I guess. But I'm not confident enough to break it.
##### Argument From Authority
> "Everything flows, nothing stands still".
Currently, it seems obligatory to include the following quote whenever talking about FRP. This rule may be relaxed in the future, I guess. But I'm neither confident enough or senior enough to break it. And whoever got fired quoting Greek philosophers, right? I'm ticking the box:
[Heraclitus](http://en.wikiquote.org/wiki/Heraclitus)
> "Everything flows, nothing stands still"
[Heraclitus](http://en.wikiquote.org/wiki/Heraclitus).
##### How FRP Happens In Reagent
To implement FRP, Reagent provides a `ratom` and a `reaction`. re-frame uses both of these building blocks, so let's make sure we understand them before going further.
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 before going further.
`ratoms` behave just like normal ClojureScript atoms. You can `swap!` and `reset!` them, `watch` them, etc.
`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 an re-frame perspective, we'll tweak that paradigm ever so slightly and **view a `ratom` as being a value that changes over time.** Seems like a subtle distinction, I know, but because of it, re-frame sees a `ratom` as an FRP Signal. [Pause and read this](http://elm-lang.org/learn/What-is-FRP.elm).
From a ClojureScript perspective, the purpose of an atom is to hold mutable data. From an re-frame perspective, we'll tweak that paradigm ever so slightly and **view a `ratom` as being 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/learn/What-is-FRP.elm).
`reaction` acts a bit like a function. It's a macro which wraps some `computation` (some block of code) and returns a `ratom` containing the result of that `computation`.
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.
Wait, what, how?
Eh, how?
Well, when a `computation` (block of 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` works out the input Signals (aka `ratoms`) for a computation, and will then automatically re-run its computation whenever one of them changes, and will `reset!` the new resulting value into the `ratom` originally returned.
To put that yet another way, a `reaction` detects a `computations` input Signals (aka input `ratoms`) and will automatically re-run that computation whenever one of them changes, and it will `reset!` the new return value into the `ratom` originally returned.
So, the `ratom` returned by a `reaction` is itself an FRP Signal. Its value will change over time as the input Signals (the dereferenced `ratoms` in the computation) change.
The `ratom` returned by a `reaction` is itself a Signal. Its value will change over time as the `computation's` input Signals change.
So, via the interplay between `ratoms` and `reactions`, values 'flow' into computations and out again, and then into other computations, etc. Data flows through the Signal graph.
So, via the interplay between `ratoms` and `reactions`, values 'flow' into computations and out again, and then into other computations, etc. "Values" flow (propagate) through the Signal graph.
But this Signal graph will be without cycles, because cycles cause mayhem! We want unidirectional data flow.
But this Signal graph will be one without cycles, because cycles cause mayhem! re-frame achieves a unidirectional flow.
While the mechanics are different, `reaction` has the intent of `lift` in [Elm] and `defc=` in [Hoplon].
@ -190,19 +198,19 @@ Right, so that was a lot of words. Some code to clarify:
(println @ratom3) ;; ==> "World" ;; ratom3 is automatically updated too.
```
So, in FRP terms, a `reaction` will produce a "stream" of values over time (it is a Signal), accessible via the `ratom` it returns.
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...
Okay, that was all important background information for what is to follow. Back to the diagram ...
### The Components
### Components
Extending the diagram a bit, we introduce `components`:
Extending the diagram, we introduce `components`:
```
app-db --> components --> Hiccup
```
When using Reagent, your primary job is to write one or more `components`.
When using Reagent, your primary job is to write one or more `components`.
Think about `components` as `pure functions` - data in, Hiccup out. `Hiccup` is
ClojureScript data structures which represent DOM. Here's a trivial component:
@ -242,7 +250,7 @@ So components are easy - at core they are a render functions which turn data int
Now, let's introduce `reaction` into this mix. On the one hand, I'm complicating things by doing this, because Reagent allows you to be ignorant of the mechanics I'm about to show you. (It invisibly wraps your components in a `reaction` allowing you to be blissfully ignorant of how the magic happens.)
On the other hand, it is useful to understand exactly how the Reagent Signal graph is wired, because in a minute, when we get to subscriptions, we'll be directly using `reaction`, so we might as well bite the bullet here and now ... and, anyway, it is pretty easy...
```Clojure
(defn greet ;; a component - data in, Hiccup out.
[name] ;; name is a ratom
@ -255,7 +263,7 @@ On the other hand, it is useful to understand exactly how the Reagent Signal gra
;; what is the result of the initial computation ?
(println @hiccup-ratom)
;; ==> [:div "Hello " "re-frame"] ;; returns hiccup (a plain old vector of stuff)
;; ==> [:div "Hello " "re-frame"] ;; returns hiccup (a vector of stuff)
;; now change 'n'
;; 'n' is an input Signal for the reaction above.
@ -263,10 +271,12 @@ On the other hand, it is useful to understand exactly how the Reagent Signal gra
;; because 'n' is dereferenced within the execution of the reaction's computation.
;; reaction notices what ratoms are dereferenced in its computation, and watches
;; them for changes.
(reset! n "blah") ;; n gets a new value
(reset! n "blah") ;; n changes
;; The reaction will be rerun...
;; ... 'hiccup-ratom' will be reset! to the new value
;; The reaction above will notice the change to 'n' ...
;; ... and will re-run its computation ...
;; ... which will have a new "return value"...
;; ... which will be "reset!" into "hiccup-ratom"
(println @hiccup-ratom)
;; ==> [:div "Hello " "blah"] ;; yep, there's the new value
```
@ -278,7 +288,7 @@ This is one-way data flow, with FRP-nature.
### Truth Interlude
I haven't been entirely straight with you:
1. Reagent re-runs `reactions` (re-computations) via requestAnimationFrame. So a re-computation happens about 16ms after the need for it is detected, or after the current thread of processing finishes, whichever is the greater. So if you are in a bREPL and you run the lines of code above one after the other too quickly, you might not see the re-computation done immediately after `n` gets reset!, because the next animationFrame hasn't run (yet). But you could add a `(reagent.core/flush)` after the reset! to force re-computation to happen straight away.
1. Reagent re-runs `reactions` (re-computations) via requestAnimationFrame. So a re-computation happens about 16ms after changed input Signals are detected, or after the current thread of processing finishes, whichever is the greater. So if you are in a bREPL and you run the lines of code above one after the other too quickly, you might not see the re-computation done immediately after `n` gets reset!, because the next animationFrame hasn't run (yet). But you could add a `(reagent.core/flush)` after the reset! to force re-computation to happen straight away.
2. `reaction` doesn't actually return a `ratom`. But it returns something that has ratom-nature, so we'll happily continue believing it is a `ratom` and no harm will come to us.
On with the rest of my lies and distortions...
@ -289,15 +299,13 @@ A `component` like `greet` is like the templates you'd find in
Django, Rails, Handlebars or Mustache -- it maps data to HTML -- except for two massive differences:
- you have the full power of ClojureScript available to you (generating a Clojure data structure). The downside is that these are not "designer friendly" HTML templates.
- these components are reactive. When their input Signals change, they
are automatically rerun, producing new Hiccup (fresh DOM!). Reagent adroitly shields you from the details, but `components` are wrapped by a `reaction`.
are automatically rerun, producing new Hiccup (fresh DOM!). Reagent adroitly shields you from the details, but the renderer of any `component` is wrapped by a `reaction`. If any of the the "inputs" to that render change, the render is rerun.
### React etc.
Okay, so we have some one-way FRP data flow happening here.
Okay, so we have some one-way, reactive, async, discrete FRP-ish data flow happening here.
Question: To which ocean does this river flow?
Answer: The DOM ocean.
Question: To which ocean does this river flow? Answer: The DOM ocean.
```
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
@ -320,17 +328,17 @@ In abstract ClojureScript syntax terms, you could squint and imagine the process
```Clojure
(-> app-db
components ;; produces Hiccup
Reagent ;; produces VDOM (virtual DOM)
Reagent ;; produces VDOM (virtual DOM that React understands)
React) ;; produces HTML (which magically and efficiently appears on the page).
```
Via `ratom` and `reaction`, changes to `app-db` are pushed into the pipeline, causing new DOM to pop out the other end, and be displayed on our page.
Via the interplay between `ratom` and `reaction`, changes to `app-db` are pushed into the pipeline, causing new DOM to pop out the other end, and be displayed on our page.
But we don't have to bother ourselves with most of the pipeline. We just write the `components` and Reagent/React will look after the rest.
### Subscribe
Interesting to get the big picture, but the first part of the flow is critical:
Whilst it is interesting to get the big picture, most of it happens "for free". We don't have to do much other than to kick start it correctly. That means understanding `subscribe`.
```
app-db --> components
@ -344,11 +352,11 @@ Let's pause to consider **our dream solution** for this part of the flow. `compo
* obtain data from `app-db` (their job is to turn this data into hiccup).
* obtain this data via a (possibly parameterised) query over `app-db`. Think database kinda query.
* automatically recompute their hiccup output, as the data returned by the query changes, over time.
* use declarative queries. Components should know as little as possible about the data structure in `app-db`. SQL? Datalog?
* use declarative queries. Components should know as little as possible about the structure of `app-db`. SQL? Datalog?
re-frame's `subscriptions` are an attempt to live this dream. As you'll see, they fall short on a couple of points, but they're not too bad.
As a re-frame app developer, your job is to write and register one or more "subscription handlers" (functions that do a named query). Your subscription functions must return a value that changes over time (a Signal). I.e. they'll be returning a reaction or, at least, the `ratom` produced by a `reaction`.
As a re-frame app developer, your job will be to write and register one or more "subscription handlers" - functions that do a named query. Your subscription functions must return a value that changes over time (a Signal). I.e. they'll be returning a reaction or, at least, the `ratom` produced by a `reaction`.
Rules:
- `components` never source data directly from `app-db`, and instead, they use a subscription.
@ -366,8 +374,8 @@ Here's a component using a subscription:
First, note this is a [Form-2](https://github.com/reagent-project/reagent/wiki/Creating-Components#form-2--a-function-returning-a-function) `component` ([there are 3 forms](https://github.com/reagent-project/reagent/wiki/Creating-Components)).
Above, we've used the simplest, Form-1 components (no setup was required, just render). With Form-2, there's a function returning a function:
- the returned function is the render function. Behind the scenes, Reagent will wrap this render function in a `reaction` to make it produce new Hiccup when its inputs change. In our case, that means it will rerun every time `name-ratom` changes.
Previously in this document, we've used the simplest, Form-1 components (no setup was required, just render). With Form-2 components, there's a function returning a function:
- the returned function is the render function. Behind the scenes, Reagent will wrap this render function in a `reaction` to make it produce new Hiccup when its input Signals change. In our example above, that means it will rerun every time `name-ratom` changes.
- the outer function is a setup function, called once to initialise the component. Notice the use of 'subscribe' with the parameter `:name-query`. That creates a Signal through which new values are supplied over time.
`subscribe` is called like this:
@ -376,9 +384,9 @@ Above, we've used the simplest, Form-1 components (no setup was required, just r
(subscribe [query-id some optional query parameters])
```
There is only one `subscribe` function. We must register our `handlers` with it.
There is only one `subscribe` function and it takes one parameter, assumed to be a vector.
The first element in the vector (`query-id`) identifies the query and the other elements are optional, query parameters. With a traditional database a query might be:
The first element in the vector (shown as `query-id`) identifies/names the query and the other elements are optional, query parameters. With a traditional database a query might be:
```
select * from customers where name="blah"
@ -402,7 +410,7 @@ For this to work, we must write and register a subscription handler for `:custom
customer-query) ;; the function which will perform the query
```
**Note**: `components` tend to be organised into a hierarchy, often with data flowing from parent to child via parameters. So not every component needs a subscription. Often the values passed in from a parent component are sufficient.
**Note**: `components` tend to be organised into a hierarchy, often with data flowing from parent to child via parameters. So not every component needs a subscription. Very often the values passed in from a parent component are sufficient.
**Rule**: subscriptions can only be used in `Form-2` components and the subscription must be in the outer setup function and not in the inner render function. So the following is **wrong** (compare to the correct version above)
@ -413,11 +421,23 @@ For this to work, we must write and register a subscription handler for `:custom
[:div "Hello" @name-ratom]))
```
Why is this wrong? Well, this component would get re-rendered every time `db-app` changed, even if the value in `name-ratom` (query result) stayed the same. Use a `Form-2` component instead.
### The Signal Graph
A bigger example ...
Imagine that our `app-db` contains some `items` - a vector of maps. And imagine that we must display these items sorted by one of their attributes. We could write this query-handler:
Imagine that our `app-db` contains some `items` - a vector of maps. And imagine that we must display these items sorted by one of their attributes.
Think of it like this:
```Clojure
(def i-list [{:name "a" :val 23 :}
{:name "b" :val 81
{:name "c" :val 23}])
(def db-app (reagent/atom {:items i-list})
``
We could write this query-handler:
```Clojure
(register
@ -429,16 +449,21 @@ Imagine that our `app-db` contains some `items` - a vector of maps. And imagine
(sort-by @sort-kw items))))) ;; return them sorted
```
First, notice that this reaction involves 2 input Signals: `db` and `sort-kw`.
Subscription handlers are given two parameters:
1. `app-db` - that's a reagent/atom which holds ALL the app's state. This is the "database" on which we perform the "query".
2. the vector originally supplied to `subscribe`.
In the example above, notice that the reaction wraps 2 input Signals: `db` and `sort-kw`.
If either changes, the query is re-run.
In a component, we could use this query via `subscribe`:
```Clojure
(defn items-list ;; outer, setup function, called once
(defn items-list ;; Form-2 component - outer, setup function, called once
[]
(let [by-this (reagent/atom :name) ;; sort by :name attribute, GUI might reset! somehow
items (subscribe [:sorted-items by-this])
(let [by-this (reagent/atom :name) ;; sort by :name attribute, GUI might reset! this somehow
items (subscribe [:sorted-items by-this]) ;; <-- subscribe called here
num (reaction (count @items))] ;; Woh! a reaction based on the subscription
(fn []
[:div
@ -446,9 +471,9 @@ In a component, we could use this query via `subscribe`:
(into [:div ] (map item-render @items))]))) ;; item-render is another component
```
There's a bit going on in that `let`, most of it highly contrived, just so I can show off chained reactions. Okay, okay. All I wanted was an excuse to use the phrase chained reactions.
There's a bit going on in that `let`, most of it highly contrived, just so I can show off chained reactions. Okay, okay. All I wanted really was an excuse to use the phrase "chained reactions".
While contrived, the calculation of `num` is interesting.
The calculation of `num` is done by a `reaction` which has `items` as a an input Signal. And, as we saw, `items` is itself a reaction over two other signals (one of them the `app-db`).
In reality, the approach taken above is inefficient. Every time `app-db` changes, the `:sorted-items` query is going to be re-run and it's going to re-sort items. But items might not have changed. Some other part of `app-db` may have changed. We don't want to re-sort items each time something unrelated changes.
@ -463,15 +488,15 @@ We can fix that up by slightly changing our subscription function:
(reaction (sort-by @sort-kw @items))))) ;; reaction #2
```
So now there's one reaction which uses the output of another reaction.
So now there's one reaction which uses the result of another reaction. The 1st reaction just extracts `items`. The 2nd one does the CPU-expensive sort.
Be aware that the second reaction will only be triggered if `@items` does not test `identical?` to the previous value. **Yes, that sort of optimisation is built into chain `reactions`.** Which means the component render function (which is wrapped in another reaction) won't rerun if `app-db` changes, unless items changes. Now we're very efficient.
That 2nd, expensive reaction will only be triggered if `@items` does not test `identical?` to the previous value. **Yes, that sort of optimisation is built into chain `reactions`.** Which, in turn, means the component render function (which is wrapped in yet another reaction) won't rerun unless `items` itself changes. Now we're very efficient. Thank you immutable data structures.
If I were doing this for real (rather than just showing possibilities), I'd probably create a simple subscription for items (unsorted), and then do the sort in the component itself (as a reaction, similar to how `num` was done in the example above). After all, it is the component which needs to show sorted, so it should explicitly do that work.
If I were doing this for real (rather than just showing possibilities), I'd probably create a simple subscription for items (unsorted), and then do the sort in the component itself (as a reaction, similar to the way `num` was done in the example above). After all, it is the component which needs the sorting, so it should explicitly do that work.
Summary:
- you can chain reactions.
- Reagent will eliminate unnecessary Signal propagation via `identical?` checks (not equality checks!). This is the nice by-product of working with immutable data structures.
- Reagent will eliminate unnecessary Signal propagation via highly efficient `identical?` checks (not equality checks!). This is the beautiful by-product of working with immutable data structures
### The 2nd Flow
@ -482,23 +507,27 @@ The data flow from `app-db` to the DOM is the first half of the story. We now ne
While the first flow has FRP-nature, the 2nd flow does not.
When I think about these two flows, I imagine [one of those school diagrams](http://thumbnails-visually.netdna-ssl.com/water-cycle_521f29b8b6271_w1500.png) showing the water cycle. Rivers taking water down to the oceans, and evaporation/clouds taking water back over the mountains to fall again as rain. Repeat.
When I think about these two flows, I imagine [one of those school diagrams](http://thumbnails-visually.netdna-ssl.com/water-cycle_521f29b8b6271_w1500.png) showing the water cycle. Rivers taking water down to the oceans, and evaporation/clouds/wind taking water back over the mountains to fall again as rain or snow. Repeat.
There is a cycle, but there's two kinds of flows.
There is a cycle, but it is handled by two independent flows.
### Event Flow
Events are what flow in the opposite direction.
In response to user interaction, a DOM will generate
events like "clicked delete button on item 42" or
"unticked the checkbox for 'send me spam'".
These events have to be "handled". The code doing this handling might
mutate app state (in `app-db`), or request more data from the server, or POST somewhere and wait for a response, etc. In fact, all these actions will ultimately result in changes to the `app-db`.
mutate app state (in `app-db`), or request more data from the server, or POST somewhere and wait for a response, etc.
In fact, all these actions ultimately result in changes to the `app-db`.
An application will have many handlers, and collectively
An application has many handlers, and collectively
they represent the **control layer of the application**.
In re-frame, the backward data flow of events happens via a conveyor belt:
In re-frame, the backwards data flow of events happens via a conveyor belt:
```
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
@ -514,8 +543,8 @@ that means the `app-db` will change. After all, it **is** the state. And the D
So that tends to be the cycle: DOM events dispatch, handlers manage them, which cause `app-db` changes, which then cause a re-render, and the users sees something different. That's our water cycle.
So handlers, which look after events, are the part of the system which do `app-db` mutation. You
could almost imagine them as a "stored procedure" in a
Because handlers are that part of the system which does `app-db` mutation, you
could almost imagine them as a "stored procedures" on a
database. Almost. Stretching it? We do like our in-memory
database analogies.
@ -538,7 +567,7 @@ Here are some other example events:
[[:complicated :multi :part :key] "a parameter" "another one" 45.6]
```
**Rule**: events are pure data. No dirty tricks like putting callbacks on the wire. You know who you are.
**Rule**: events are pure data. No dirty tricks like putting callback functions on the wire. You know who you are.
### Dispatching Events
@ -554,15 +583,15 @@ For example, a button component might be like this:
"Yes"])
```
Notice the `on-click` handler:
Notice the `on-click` DOM handler:
```Clojure
#(dispatch [:yes-button-clicked])
```
With re-frame, we try to keep the DOM as passive as possible. It is simply a rendering of `app-db`. So that "on-click" is a simple as we can make it.
With re-frame, we try to keep the DOM as passive as possible. So that "on-click" is a simple as we can make it.
There is a single `dispatch` function in the entire framework, and it takes only one parameter, the event vector.
There's a single `dispatch` function in the entire framework, and it takes one parameter: the event to be dispatched (which is pure simple, lovely data).
Let's update our diagram to show `dispatch`:
@ -570,35 +599,55 @@ Let's update our diagram to show `dispatch`:
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
^ |
| v
handlers <---------------------------------------- (dispatch [event-id other params])
handlers <---------------------------------------- (dispatch [event-id event params])
```
**Rule**: `components` are as passive and minimal as possible when it comes to handling events. They `dispatch` and nothing more.
**Rule**: `components` are as passive and minimal as possible when it comes to handling events. They `dispatch` pure data and nothing more.
### Event Handlers
Collectively, event handlers provide the control logic in a re-frame application.
Almost all event handlers mutate `app-db` in some way: adding an item here, or deleting that one there. So, often CRUD, but sometimes much more, and sometimes with async results.
An event handler is a pure function of two parameters (*** not true in reference impl, yet ***):
1. current value in `app-db`
2 an event (represented as a vector)
Even though handlers appear to be about `app-db` mutation, re-frame requires them to be pure functions with a signature of:
```
(state-of-app-db, event-vector) -> new-state
```
re-frame passes to an event handler two parameters: the current state of `app-db` plus the event, and the job of a handler to return a modified version of the state (which re-frame will then put back into the `app-db`). **XXX currently not true but it will be shortly.**
It returns the new value which should be reset! into `app-db`.
An example handler:
```Clojure
(defn handle-delete
[state [_ item-id]] ;; notice how event vector is destructured -- 2nd parameter
(dissoc-in state [:some :path item-id])) ;; return a modified version of 'state'
[app-state [_ item-id]] ;; notice how event vector is destructured -- 2nd parameter
(dissoc-in app-state [:some :path item-id])) ;; return a modified version of 'app-state'
```
Because handlers are pure functions, and because they generally only have to handle one situation, they tend to be easy to test and understand.
Handling an event invariably involves mutating the value in `app-db` (which is provided as the first parameter). An item is added here, or one is deleted there. So, often simple CRUD, but sometimes much more, and sometimes with async results.
But the `app-db` mutation is ultimately handled by re-frame (it does the `reset!). That leaves your event handlers pure. As a result, they tend to be easy to test and understand. Many are almost trivial.
### Routing
`dispatch` has to call the right handler for each event. Named event handlers have to be registered.
When `dispatch` is passed an event vector, it just puts that event onto a conveyor belt.
The consumer on the end of the conveyor is a `router` which will organise to for that event to be processed by the right handler.
```
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
^ |
| v
handlers <----- router <----------------------- (dispatch [event-id event params])
```
The `router` will:
1. inspect the 1st element of the arriving vector
2. look in its registry for the handler which looks after this kind of event
3. call that handler with two parameters: (1) the current value in `app-db` and (2) the event vector
4. reset! the returned value back into `app-db`.
As a re-frame app developer, your job is to write handlers for each kind of event, and then to register those handlers with the router.
Here's how we would register our event handler:
```Clojure
(register
@ -606,24 +655,35 @@ Because handlers are pure functions, and because they generally only have to han
handle-delete) ;; the handler function for that event
```
### State Transition
Any arriving event vector which has `:delete-item` as the first element will now be routed to our handler.
Above, I commented that collectively handlers represent the "control layer" of the application.
### Control Via FSM
Most of what they do is to manage state transitions. Eg: the application is in state X, and event E arrives, so the handler moves the app to state Y.
Above, I commented that event handlers collectively represent the "control layer" of the application. They contain logic which interprets arriving events and they "step" the application "forward" via mutations to `app-db`.
Although I've done nothing to try and implement it yet, this appears beautifully setup for using [statecharts](http://www.amazon.com/Constructing-User-Interface-Statecharts-Horrocks/dp/0201342782).
Our `delete-handler` example above is trivial, but as an application grows more features, the logic in many handlers will become more complicated, and they will have to query BOTH the current state of the app AND the arriving event vector to determine what action to take.
At that point, we're implementing a [Finite State Machine](http://en.wikipedia.org/wiki/Finite-state_machine):
- your app is in a certain logical state (defined by the current values in `app-db`)
- the arriving event vector represents a `trigger`.
- the event handler implements "a transition" subject to BOTH the current logical state and the arriving trigger.
- after the handler has run, the transition may have moved the app into a new logical state.
- Repeat.
Not every GUI has lots of logical `states`, but many do, and if you are implementing one of them, then formally recognising it and using a technque like [statecharts](http://www.amazon.com/Constructing-User-Interface-Statecharts-Horrocks/dp/0201342782) will help greatly in getting a clean design.
The beauty of re-frame from a FSM point of view is that re-frame stores all its data in one place - unlike OO systems where the data is distributed (and synchronized) across many objects. So implementing your control logic as a FSM is possible and natural in re-frame, whereas it is often difficult and contrived to do so in other kinds of architecture.
### Talking To A Server
Some events handlers will need to initiate an async server connection (e.g. GET or POST something).
The initiating event handlers should organise that the `on-success` or `on-fail` handlers for these HTTP requests themselves simply dispatch an event.
The initiating event handlers should organise that the `on-success` or `on-fail` handlers for these HTTP requests themselves simply dispatch an event.
**Notes**:
- all events are handled via a call to `dispatch`. GUI events, async HTTP events, everything.
- `dispatch` will cause a handler function to be called. But the process is async. The call is queued.
- if you (further) dispatch in handler, then that will be async too. The associated handler is queued for calling later. Why? Partially because handlers are given a snapshot of the `app-db` and can't be nested.
- if you (further) dispatch in a handler, then that will be async too. The associated handler is queued for calling later. Why? Partially because handlers are given a snapshot of the `app-db` and can't be nested.
- if you kick off an HTTP request in a handler, then organise for the on-success or on-fail handlers to dispatch their outcome. All events are handled via dispatch.
- if a handler does a lot of work and hogs the thread, this will freeze the GUI because browsers only give us one execution thread . **XXX Nice Solution needed. **
@ -651,6 +711,3 @@ All the parts are lovely and simple. And they plug together nicely.
[Hoplon]:http://hoplon.io/
[Pedestal App]:https://github.com/pedestal/pedestal-app
[Herbert Schema]:https://github.com/miner/herbert