Back to old ways

This commit is contained in:
mike-thompson-day8 2015-01-13 18:21:08 +11:00
parent adc26e9b3a
commit d0a657299a
1 changed files with 32 additions and 31 deletions

View File

@ -7,17 +7,17 @@ Todo:
## re-frame
**re-frame** is a tiny [Reagent] framework for writing [SPAs] using ClojureScript.
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** for one version of this pattern.
**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 (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.
The re-frame pattern is simple. So simple, in fact, that the reference implementation in this repo is barely 100 lines of code.
To build an app using **re-frame**, you:
To build an app using re-frame, you:
- design your app's data structure (data layer)
- write and register subscription functions (query layer)
- write Reagent component functions (view 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, uses conveyor belts, statecharts and claims a 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, routing to server-side services, etc. It is just about writing client side apps. That doesn't mean it 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, routing to server-side services, etc. It is just about writing client side apps. That doesn't mean it wouldn't work well when a server is heavily 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 tweak in the direction you need.
Remember, re-frame is more of a pattern than an implementation, so you can easily tweak in the 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
@ -42,7 +42,7 @@ squint a little to see the benefits that accrue at larger scale.
### Nothing New
Nothing about **re-frame** is particularly original or clever. You'll find
Nothing about re-frame is particularly original or clever. You'll find
no ingenious use of functional zippers, transducers or `core.async`. And this is a good thing (although, for the record, one day I'd love to develop
something original and clever).
@ -58,7 +58,7 @@ Reagent when you view it as an FRP 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.
Finally, we believe in one-way data flow. No cycles! We don't like read/write `cursors` which promote two way flow of data. **re-frame** does implement two data way flow, but it
Finally, we believe in one-way data flow. No cycles! We don't like read/write `cursors` which promote two way flow of data. re-frame does implement two data way flow, but it
uses two, separate, one-way flows to achieve it, and those two flows
are different in nature.
@ -66,7 +66,7 @@ If you are curious about FRP, I'd recommend [this FRP backgrounder](https://gist
## The Parts
To explain **re-frame**, I'll incrementally develop a diagram, explaining each part as it is added.
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
@ -80,13 +80,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/miner/herbert).
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
@ -109,7 +109,7 @@ database atomically, etc. So "in-memory database"
seems a more useful paradigm than plain old atom.
Finally, a clarification: `app-db` doesn't actually have to be a reagent/atom containing
a map. In theory, **re-frame**
a map. In theory, re-frame
imposes no requirement here. It could be a [datascript] database (approach untested). But, as you'll see, it
does have to be a "reactive datastore" (one that can tell you when it has changed). In fact, `app-db` doesn't have to be a single atom either -- the pattern allows for as many as you like, although our implementation assumes one.
@ -123,13 +123,13 @@ I'm going to quote verbatim from Elm's website:
##### The Background Magic
Reagent provides a `ratom` and a `reaction`. These are **two key building blocks** for **re-frame**, so let's make sure we understand them.
Reagent provides a `ratom` and a `reaction`. These are **two key building blocks** for re-frame, so let's 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 an **re-frame** perspective, we'll tweak that paradigm ever so slightly and **view an `ratom` as being a value that changes over time.** Seems like a subtle distinction, I know, but because of it re-frame sees an `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 an FRP 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 an `ratom` containing the result of that `computation`.
`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 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.
@ -159,10 +159,10 @@ Right, so that was a lot of words. Some code to clarify:
0 "World"
1 "Hello")))
;; Notice that both computations above involve de-referencing an ratom:
;; Notice that both computations above involve de-referencing a ratom:
;; - app-db in one case
;; - ratom1 in the other
;; Notice that both reactions above return an ratom.
;; 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
@ -215,10 +215,10 @@ Here is a slightly more interesting (parameterised) component (function):
[name] ;; 'name' is a ratom holding a string
[:div "Hello " @name]) ;; dereference 'name' to extract the contained value
;; create an ratom, containing a string
;; create a ratom, containing a string
(def n (reagent/atom "re-frame"))
;; call our `component` function, passing in an ratom
;; call our `component` function, passing in a ratom
(greet n)
;; ==> [:div "Hello " "re-frame"] returns a vector
```
@ -265,7 +265,7 @@ This is one-way data flow, with FRP-nature.
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.
2. `reaction` doesn't actually return an `ratom`. But it returns something that has ratom-nature, so we'll happily continue believing it is an `ratom` and no harm will come to us.
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...
@ -332,9 +332,9 @@ So let's pause to consider **our dream solution** for this part of the flow. `co
* 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?
**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.
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 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`.
Rules:
- `components` never source data directly from `app-db`, and instead, they use a subscription.
@ -370,7 +370,7 @@ The first element in the vector (`query-id`) identifies the query and the other
select * from customers where name="blah"
```
In **re-frame** land, that would be done as follows:
In re-frame land, that would be done as follows:
(subscribe [:customer-query "blah"])
which would return a `ratom` holding the customer state (a value which might change over time!).
@ -462,7 +462,7 @@ Summary:
### The 2nd Flow
At the top, I said that **re-frame** had two data flows.
At the top, I said that re-frame had two data flows.
The data flow from `app-db` to the DOM is the first half of the story. We now need to consider the 2nd part of the story: the flow in the opposite direction.
@ -484,7 +484,7 @@ These events have to be "handled". The code doing this handling might
An application will have 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 backward data flow of events happens via a conveyor belt:
```
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
@ -546,7 +546,7 @@ Notice the `on-click` handler:
#(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. It is simply a rendering of `app-db`. 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.
@ -563,16 +563,16 @@ app-db --> components --> Hiccup --> Reagent --> VDOM --> React -->
### Event Handlers
Collectively, event handlers provide the control logic in a **re-frame** application.
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.
Even though handlers appear to be about `app-db` mutation, **re-frame** requires them to be pure functions with a signature of:
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.**
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.**
```Clojure
(defn handle-delete
@ -616,7 +616,7 @@ The initiating event handlers should organise that the `on-success` or `on-fail`
### In Summary
To build an app using **re-frame**, you'll have to:
To build an app using re-frame, you'll have to:
- design your app's data structure.
- write and register subscription functions (query layer).
- write component functions (view layer).
@ -639,3 +639,4 @@ All the parts are lovely and simple. And they plug together nicely.
[Herbert Schema]:https://github.com/miner/herbert