Readme WIP

This commit is contained in:
mike-thompson-day8 2014-12-16 00:02:27 +11:00
parent a2456f7763
commit 8513178cca
1 changed files with 42 additions and 34 deletions

View File

@ -15,17 +15,17 @@ it is instead a functional RACES framework - Reactive-Atom Component Event Subsc
### Claims
Nothing about re-frame is the slightest bit original or clever.
You'll find no ingenious use of functional zippers, transducers or core.async.
Nothing about re-frame is the slightest bit original or clever. All the ideas are copies and
you'll find no ingenious use of functional zippers, transducers or core.async.
This is a good thing (although, for the record, one day I'd love to develop
something original and clever).
Using re-frame, you will be able to break your application code into distinct
pieces, and those distinct pieces will be be pure functions.
Each can be easily described, understood and tested independently.
Each can be easily described, understood and tested independently.
At small scale, any framework seems like pesky overhead. The
explanatory examples in here are small scale, so you'll need to
explanatory examples in here are necessarily small scale, so you'll need to
squint a little to see the benefit.
### Core Beliefs
@ -34,7 +34,7 @@ First, above all we believe in the one true [Dan Holmsand] (creator of reagent),
and his divine instrument the `ratom`. We genuflect towards Sweden once a day.
Second, we believe that [FRP] is a honking great idea. When you start with reagent, you
might be tempted to see it as simply another of the React warappers (a sibling to OM and [quiescent](https://github.com/levand/quiescent)). But I think you only really "get" Reagent when you view it as an [FRP] library. To put that another way, we think that Reagent, at its best, is closer in
might be tempted to see it as simply another of the React warappers (a sibling to [OM] and [quiescent](https://github.com/levand/quiescent)). But I think you only really "get" 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]. This wasn't obvious to us initially - we
knew we liked reagent, but it took a while for the penny to drop as to why.
@ -42,7 +42,7 @@ Finally, we believe in one way data flow. We don't like read/write `cursors` wh
promote two way flow of data. re-frame does implement two data way flow, but it
uses two, seperate, one-way flows to do it.
If you aren't familiar with FRP, I'd recomend [this FRP backgrounder](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754 before you go any further.
If you are curious about FRP, I'd recomend [this FRP backgrounder](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754 before you go any further.
### At A High Level
@ -51,13 +51,15 @@ When you use re-frame, you'll be writting three kinds of functions:
- components - which turn data into DOM
- event handlers - which provide the state transition (control) layer
You'll also be choosing a certain data structure to represent the app state. XXX
You'll also be designing a data structure to represent the app state.
## The Parts
To explain re-frame, we'll now incrementally
develop a diagram. We'll explain each part as it is added.
Along the way, I'll deal with [reagent] at an intermediate to advanced level. This is not an intro to reagent tutorial.
<blockquote class="twitter-tweet" lang="en"><p>Well-formed Data at rest is as close to perfection in programming as it gets. All the crap that had to happen to put it there however...</p>&mdash; Fogus (@fogus) <a href="https://twitter.com/fogus/status/454582953067438080">April 11, 2014</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
@ -68,8 +70,7 @@ Our re-frame diagram starts with the "well formed data at rest" bit:
app-db
```
re-frame recomends that you put your data into one place (probably one dirty great
big atom) which we'll call `app-db`. Structure the data in that place, of course.
re-frame recomends that you put your data into one place which we'll call `app-db`. Structure the data in that place, of course.
Now, this advice is not the slightest bit controversial for 'real' databases, right?
You'd happily put all your well formed data into Postgres or mysql. But within a running application (in memory), it is different. If you have
@ -85,7 +86,7 @@ From here on, we'll assume `app-db` is one of these:
(def app-db (reagent/atom {})) ;; a reagent atom, containing a map
```
Although it is a reagent atom (ratom), I'd encourage you to think of it as an (in-memory) database.
Although it is a reagent atom (ratom), I'd encourage you to think of it as a poor man's, in-memory database.
It will contain structured data (perhaps with a formal [Prismatic Schema] spec).
You will need to query that data. You will perform CRUD
and other transformations on it. You'll often want to transact on this
@ -94,7 +95,7 @@ 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
imposes no requirement here. It could be a [datascript] database. But, as you'll see, it
imposes no requirement here. It could be a [datascript] database (untested). But, as you'll see, it
does have to be a "reactive datastore" (one that can tell you when it has changed). In truth, `app-db` doesn't really have to be a single atom -- the pattern allows for as many as you like, although our implementation assumes one.
##### The Bit Of Magic
@ -107,7 +108,7 @@ Reagent provides a `ratom` (reagent atom) and a `reaction`. These are two key bu
The magic bit is that `reaction` will automatically rerun the `computation` whenever the computation's "inputs" change, and then `reset!` the originally returned `ratom` to the newly conputed value.
Perhaps some code will help:
Perhaps some code will clarify:
```clojure
(ns example1
@ -133,7 +134,7 @@ Perhaps some code will help:
(println @ratom3) ;; ==> "World" ;; ratom3 is automatically updated too.
```
So, `reaction` wraps a computation, and puts the result in a returned `ratom`. Whenever the "inputs" to the computation change, the computation is rerun to calculate a new value, which is then `reset!` into the returned `ratom`. The "inputs" to the computation are any ratoms dereferenced during execution of the computation.
So, `reaction` wraps a computation, and puts the result in a returned `ratom`. Whenever the "inputs" to the computation change, the computation is rerun to calculate a new value, which is then `reset!` into that returned `ratom`. The "inputs" to the computation are any ratoms dereferenced during execution of the computation.
While the mechanics are different, this is similar in intent to `lift' in [Elm] and `defc=` in [hoplon].
@ -163,12 +164,12 @@ And if we call it:
;; ==> [:div "Hello ratoms and recactions"]
```
You'll notice that our component is a regular clojure function, nothing special. In this case, it takes no paramters and it returns a clojurescript vector (hiccup).
You'll notice that our component is a regular clojure function, nothing special. In this case, it takes no parameters and it returns a clojurescript vector (hiccup).
Here is a slightly more interesting (parameterised) component :
```
(defn greet ;; this greet has a parameter
[name] ;; 'name' is a ratom, and contains a string
[name] ;; 'name' is a ratom containing a string
[:div "Hello " @name]) ;; dereference name here to extract the value it contains
;; create a ratom, containing a string
@ -179,9 +180,11 @@ Here is a slightly more interesting (parameterised) component :
;; ==> [:div "Hello " "re-frame"] returns a vector
```
So components are simple - a function which turns data into hiccup.
So components are easy - they are functions which turns data into hiccup.
Now, we're now going to introduce `reaction` into the mix. On the one hand I'm complicating things by doing this, because reagent invisibly wraps your components in a `reaction` allowing you to be blissfully ignorant of how the magic happens. On the other hand, it is nice to understand how it all works. AND, in a minute, when we get to subscriptions, we'll be the ones actively using `reaction`. So, we might as well bite the bullet here ... and, anyway, its easy ...
Now, we're now going to introduce `reaction` into the mix. On the one hand I'm complicating things by doing this, because reagent 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 how it all works. AND, in a minute, when we get to subscriptions, we'll be the ones actively using `reaction`. So, we might as well bite the bullet here ... and, anyway, its easy ...
```
(defn greet
[name] ;; name is a ratom
@ -205,13 +208,13 @@ Now, we're now going to introduce `reaction` into the mix. On the one hand I'm
;; ==> [:div "Hello " "blah"]
```
So, as `n` changes value, the output of the computation (greet n) changes, and so the value in `hiccup-ratom` changes. One way data flow. With our FRP glasses on, we would see a series of changes to `n` as producing a "stream" of changes in `hiccup-ratom` (over time).
So, as `n` changes value, the output of the computation `(greet n)` changes, and so too the value in `hiccup-ratom` changes. One way data flow. With our FRP glasses on, we would see a series of changes to `n` as producing a "stream" of changes in `hiccup-ratom` (over time).
Note: `n` is an "input" to the computation because it is a ratom which is dereferenced within the computation.
Truth time. I haven't been entirely straight with you.
1. reagent re-runs `reactions` (re-computations) via requestAnnimationFrame. That means 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 were to actually run the lines of code above one after the other quickly, you might not see the re-computation done immediately after `n` gets reset!, because the annimationFrame hasn't run (yet). You could add a (reagent.core/flush) after the reset! that would force the 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 halm will come to us.
Truth time. I haven't been entirely straight with you:
1. reagent re-runs `reactions` (re-computations) via requestAnnimationFrame. That means 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 were to actually 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 annimationFrame hasn't run (yet). You could add a `(reagent.core/flush)` after the reset! that would force the 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 ...
@ -254,11 +257,11 @@ In abstract terms, you could squint and imagine the process as:
Via `ratom` and `reaction`, changes to `app-db` are pushed into the pipeline, causing new HTML to pop out the other end, and then onto our page. One way data flow, FRP in nature.
But, just to be clear, we don't have to bother ourselves with most of the pipeline. We just write the `components` part (pure functions!) and Reagent/React looks after the rest.
But, just to be clear, we don't have to bother ourselves with most of the pipeline. We just write the `components` part (pure functions) and Reagent/React looks after the rest.
### Subscribe
The data flow is kickstarted by subscriptions.
The data flow is kickstarted by `subscriptions`.
```
app-db --> components --> hiccup
@ -268,9 +271,9 @@ Our dream situation is for `components` to:
* 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 query)
* automatically recompute their hiccup output, as the data returned by the query changes, over time.
* the query should be as declarative as possible. We want components knowing as little as possible about the data structure in `app-db`.
* the query should be as declarative as possible. We want components knowing as little as possible about the data structure in `app-db`. SQL? Datalog?
`subscriptions` are a modest and incomplete attempt at living the dream.
`subscriptions` are a modest and incomplete attempt at living that dream.
You write and register subscriptions. `components` use subsciptions to sourcce data.
`components` never talk directly to `app-db`.
@ -286,7 +289,7 @@ Here's a component which uses a subscription:
First, note this is a form-2 `component` (there are 3 forms). Perviously, we've used the simplest, form-1 components (no setup was required). With form-2, there's a function returning a function:
- the returned function is the render fucntion. Behind the scenes, reagent will wrap this render fucntion 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.
- the outer function is a setup function, called once to initialise the component. Notice the use of 'subscribe' with the a parameter `:name-query`. That creates a stream through which new values are supplied over time.
- 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 stream through which new values are supplied over time.
`subscribe` is called like this:
```
@ -308,7 +311,7 @@ We write and register the query functions. It is our job to make the queries rea
name-query) ;; the query function
```
`components` tend to be organised into a heirarchy, with data flowing from parent to child via paramters. So not every component needs a subscription. In fact, very few do. The root component will certainly have a subscription, but apart from that not many.
`components` tend to be organised into a heirarchy, with data flowing from parent to child via paramters. So not every component needs a subscription. In fact, very few do, although the root component will certainly have one.
### Event Flow
@ -337,7 +340,7 @@ app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
Generally, when the user manipulates the GUI, the state of the application changes. In our case,
that means the `app-db` will change. After all, it **is** the state. And the DOM presented to the user is a function of that state. So that tends to be the cycle. GUI events cause `app-db` changes, which then causes a rerender, and the users sees something different.
that means the `app-db` will change. After all, it **is** the state. And the DOM presented to the user is a function of that state. So that tends to be the cycle: GUI events cause `app-db` changes, which then cause a rerender, and the users sees something different.
So handlers, which look after events, are the part of the system which does `app-db` mutation. You
could almost imagine them as a "stored procedure" in a
@ -395,11 +398,11 @@ app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
Collectively, event handlers provide the control logic in the applications.
The job of many event handlers is to change the `app-db` in some way. Add an item here, or delete that one there. So often CRUD but sometimes much more.
The job of many event handlers is to change the `app-db` in some way. Add an item here, or delete that one there. So often CRUD, but sometimes much more.
Even though handlers appear to be about `app-db` mutation, re-frame requires them to be pure fucntions with a signature:
Even though handlers appear to be about `app-db` mutation, re-frame requires them to be pure fucntions with a signature of:
```
(state-in-app-db, event-vector) -> new-state
(state-of-app-db, event-vector) -> new-state
```
re-frame passes to an event handler two paramters: the current state of `app-db` plus the event, and the job of a handler to to return a modified version of the state (which re-frame will then put back into the `app-db`).
@ -419,11 +422,17 @@ A big part of what they do is to manage state transitions. The application is in
Although I've done nothing to try and implement it, this is obviously fertile territory for using [statechars](http://www.amazon.com/Constructing-User-Interface-Statecharts-Horrocks/dp/0201342782).
### Talking To The Server
When async events occur, like POST responses, an event get dispatched, just as when a DOM event happens.
State changes/transitions only happen via dispatch.
### Routing
`dispatch` has to call the right handler.
`dispatch` has to call the right handler. Handlers have to be registered.
XXXX handlers have to be registered
XXX Example.
### In Summary
@ -441,7 +450,6 @@ To use re-frame, you'll have to:
[Elm]:http://elm-lang.org/
[OM]:https://github.com/swannodette/om
[Prismatic Schema]:https://github.com/Prismatic/schema
[InterViews]:http://www.softwarepreservation.org/projects/c_plus_plus/library/index.html#InterViews
[datascript]:https://github.com/tonsky/datascript
[Hoplon]:http://hoplon.io/
[Pedestal App]:https://github.com/pedestal/pedestal-app