Merge branch 'develop'
This commit is contained in:
commit
9a93a8eec4
|
@ -1,4 +1,4 @@
|
|||
.idea/**/*
|
||||
**/.idea/**/*
|
||||
!.idea/codeStyleSettings.xml
|
||||
*.iml
|
||||
*.log
|
||||
|
|
|
@ -10,13 +10,13 @@
|
|||
re-frame puts all your application state into one place, which is
|
||||
called `app-db`.
|
||||
|
||||
Ideally, you will provide a spec for this data-in-the-one-place,
|
||||
Ideally, you will provide a spec for this data-in-the-one-place,
|
||||
[using a powerful and leverageable schema](http://clojure.org/about/spec).
|
||||
|
||||
Now, this advice is not the slightest bit controversial for 'real' databases, right?
|
||||
You'd happily put all your well-formed data into PostgreSQL.
|
||||
|
||||
But within a running application (in memory), there can be hesitation. If you have
|
||||
But within a running application (in memory), there can be hesitation. If you have
|
||||
a background in OO, this data-in-one-place
|
||||
business is a really, really hard one to swallow. You've
|
||||
spent your life breaking systems into pieces, organised around behaviour and trying
|
||||
|
@ -39,15 +39,15 @@ seems a more useful paradigm than plain old map-in-atom.
|
|||
|
||||
Further Notes:
|
||||
|
||||
1. `app-state` would probably be a more accurate name, but I choose `app-db` instead because
|
||||
1. `app-state` would probably be a more accurate name, but I choose `app-db` instead because
|
||||
I wanted to convey the in-memory database notion as strongly as possible.
|
||||
2. In the documentation and code, I make a distinction between `app-db` (the `ratom`) and
|
||||
2. In the documentation and code, I make a distinction between `app-db` (the `ratom`) and
|
||||
`db` which is the (map) `value` currently stored **inside** this `ratom`. Be aware of that naming as you read code.
|
||||
3. re-frame creates and manages an `app-db` for you, so
|
||||
you don't need to declare one yourself (see the [the first FAQ](FAQs/Inspecting-app-db.md) if you want
|
||||
you don't need to declare one yourself (see the [the first FAQ](FAQs/Inspecting-app-db.md) if you want
|
||||
to inspect the value it holds).
|
||||
4. `app-db` doesn't actually have to be a `ratom` containing a map. It could, for example,
|
||||
be a [datascript database](https://github.com/tonsky/datascript). In fact, any database which
|
||||
4. `app-db` doesn't actually have to be a `ratom` containing a map. It could, for example,
|
||||
be a [datascript database](https://github.com/tonsky/datascript). In fact, any database which
|
||||
can signal you when it changes would do. We'd love! to be using [datascript database](https://github.com/tonsky/datascript) - so damn cool -
|
||||
but we had too much data in our apps. If you were to use it, you'd have to tweak re-frame a bit and use [Posh](https://github.com/mpdairy/posh).
|
||||
|
||||
|
@ -55,35 +55,35 @@ Further Notes:
|
|||
### The Benefits Of Data-In-The-One-Place
|
||||
|
||||
1. Here's the big one: because there is a single source of truth, we write no
|
||||
code to synchronize state between many different stateful components. I
|
||||
code to synchronise state between many different stateful components. I
|
||||
cannot stress enough how significant this is. You end up writing less code
|
||||
and an entire class of bugs is eliminated.
|
||||
(This mindset is very different to OO which involves
|
||||
distributing state across objects, and then ensuring that state is synchronized, all the while
|
||||
distributing state across objects, and then ensuring that state is synchronised, all the while
|
||||
trying to hide it, which is, when you think about it, quite crazy ... and I did it for years).
|
||||
|
||||
2. Because all app state is coalesced into one atom, it can be updated
|
||||
with a single `reset!`, which acts like a transactional commit. There is
|
||||
an instant in which the app goes from one state to the next, never a series
|
||||
of incremental steps which can leave the app in a temporarily inconsistent, intermediate state.
|
||||
2. Because all app state is coalesced into one atom, it can be updated
|
||||
with a single `reset!`, which acts like a transactional commit. There is
|
||||
an instant in which the app goes from one state to the next, never a series
|
||||
of incremental steps which can leave the app in a temporarily inconsistent, intermediate state.
|
||||
Again, this simplicity causes a certain class of bugs or design problems to evaporate.
|
||||
|
||||
3. The data in `app-db` can be given a strong schema
|
||||
so that, at any moment, we can validate all the data in the application. **All of it!**
|
||||
We do this check after every single "event handler" runs (event handlers compute new state).
|
||||
And this enables us to catch errors early (and accurately). It increases confidence in the way
|
||||
so that, at any moment, we can validate all the data in the application. **All of it!**
|
||||
We do this check after every single "event handler" runs (event handlers compute new state).
|
||||
And this enables us to catch errors early (and accurately). It increases confidence in the way
|
||||
that Types can increase confidence, only [a good schema can potentially provide more
|
||||
**leverage** than types](https://www.youtube.com/watch?v=nqY4nUMfus8).
|
||||
**leverage** than types](https://www.youtube.com/watch?v=nqY4nUMfus8).
|
||||
|
||||
4. Undo/Redo [becomes straight forward to implement](https://github.com/Day8/re-frame-undo).
|
||||
It is easy to snapshot and restore one central value. Immutable data structures have a
|
||||
4. Undo/Redo [becomes straight forward to implement](https://github.com/Day8/re-frame-undo).
|
||||
It is easy to snapshot and restore one central value. Immutable data structures have a
|
||||
feature called `structural sharing` which means it doesn't cost much RAM to keep the last, say, 200
|
||||
snapshots. All very efficient.
|
||||
For certain categories of applications (eg: drawing applications) this feature is borderline magic.
|
||||
Instead of undo/redo being hard, disruptive and error prone, it becomes trivial.
|
||||
**But,** many web applications are not self contained
|
||||
data-wise and, instead, are dominated by data sourced from an authoritative, remote database.
|
||||
For these applications, re-frame's `app-db` is mostly a local caching
|
||||
snapshots. All very efficient.
|
||||
For certain categories of applications (eg: drawing applications) this feature is borderline magic.
|
||||
Instead of undo/redo being hard, disruptive and error prone, it becomes trivial.
|
||||
**But,** many web applications are not self contained
|
||||
data-wise and, instead, are dominated by data sourced from an authoritative, remote database.
|
||||
For these applications, re-frame's `app-db` is mostly a local caching
|
||||
point, and being able to do undo/redo its state is meaningless because the authoritative
|
||||
source of data is elsewhere.
|
||||
|
||||
|
@ -97,15 +97,15 @@ source of data is elsewhere.
|
|||
You need to create a [spec](http://clojure.org/about/spec) schema for `app-db`. You want that leverage.
|
||||
|
||||
Of course, that means you'll have to learn [spec](http://clojure.org/about/spec) and there's
|
||||
some overhead in that, so maybe, just maybe, in your initial experiments, you can
|
||||
some overhead in that, so maybe, just maybe, in your initial experiments, you can
|
||||
get away without one. But not for long. Promise me you'll write a `spec`. Promise me. Okay, good.
|
||||
|
||||
Soon we'll look at the [todomvc example](https://github.com/Day8/re-frame/tree/master/examples/todomvc)
|
||||
which shows how to use a spec. (Check out `src/db.cljs` for the spec itself, and then in `src/events.cljs` for
|
||||
how to write code which checks `app-db` against this spec after every single event has been
|
||||
how to write code which checks `app-db` against this spec after every single event has been
|
||||
processed.)
|
||||
|
||||
Specs are potentially more leveragable than types. This is a big interesting idea which is not yet mainstream.
|
||||
Specs are potentially more leveragable than types. This is a big interesting idea which is not yet mainstream.
|
||||
Watch how: <br>
|
||||
https://www.youtube.com/watch?v=VNTQ-M_uSo8
|
||||
|
||||
|
@ -116,7 +116,7 @@ https://vimeo.com/195711510
|
|||
|
||||
See [FAQ #1](FAQs/Inspecting-app-db.md)
|
||||
|
||||
***
|
||||
***
|
||||
|
||||
Previous: [This Repo's README](../README.md)
|
||||
Up: [Index](README.md)
|
||||
|
|
|
@ -66,7 +66,7 @@ Then:
|
|||
So, what's just happened? The ClojureScript code under `src` has been compiled across to `javascript` and
|
||||
put into `/resources/public/js/client.js` which is loaded into `/resources/public/example.html` (the HTML you just openned)
|
||||
|
||||
Figwheel provides for hot-loading, so you can edit the source and watch the loaded HTML change.
|
||||
Figwheel provides for hot-loading, so you can edit the source and watch the loaded HTML change.
|
||||
|
||||
|
||||
## Namespace
|
||||
|
@ -110,7 +110,7 @@ format for events. For example:
|
|||
[:delete-item 42]
|
||||
```
|
||||
|
||||
The first element in the vector identifies the `kind` of `event`. The
|
||||
The first element in the vector is a keyword which identifies the `kind` of `event`. The
|
||||
further elements are optional, and can provide additional data
|
||||
associated with the event. The additional value above, `42`, is
|
||||
presumably the id of the item to delete.
|
||||
|
@ -121,8 +121,7 @@ Here are some other example events:
|
|||
[:dressing/put-pants-on "velour flares" {:method :left-leg-first :belt false}]
|
||||
```
|
||||
|
||||
The `kind` of event is a keyword, and for non-trivial
|
||||
applications it will be namespaced.
|
||||
(For non-trivial applications, the `kind` keyword will be namespaced.)
|
||||
|
||||
**Rule**: events are pure data. No sneaky tricks like putting
|
||||
callback functions on the wire. You know who you are.
|
||||
|
@ -172,65 +171,62 @@ for each kind of event.
|
|||
Collectively, event handlers provide the control logic in a re-frame application.
|
||||
|
||||
In this application, 3 kinds of event are dispatched:
|
||||
`:initialise`
|
||||
`:initialize`
|
||||
`:time-color-change`
|
||||
`:timer`
|
||||
|
||||
3 events means we'll be registering 3 event handlers.
|
||||
3 events means we'll be registering 3 event handlers.
|
||||
|
||||
### Two ways To register
|
||||
### Two ways to register
|
||||
|
||||
Event handlers can be registered via either `reg-event-db`
|
||||
or `reg-event-fx` (`-db` vs `-fx`).
|
||||
Handler functions take `coeffects` (input args) and return `effects`.
|
||||
|
||||
Handler functions take `coeffects` (input args) and return `effects`,
|
||||
however `reg-event-db` allows you to write simpler handlers.
|
||||
The handler functions it registers (1) take just one `coeffect` -
|
||||
the current app state, and (2) return only one `effect` -
|
||||
the updated app state.
|
||||
|
||||
Whereas `reg-event-fx` registered handlers are more flexible.
|
||||
Handlers can be registered via either `reg-event-fx`
|
||||
or `reg-event-db` (`-fx` vs `-db`):
|
||||
|
||||
Because of its simplicity, we'll be using the former here.
|
||||
- `reg-event-fx` can take multiple `coeffects` and can return multiple `effects`, while
|
||||
- `reg-event-db` allows you to write simpler handlers in the common case where you want
|
||||
them to take only one `coeffect` - the current app state - and return one `effect` - the
|
||||
updated app state.
|
||||
|
||||
Because of its simplicity, we'll be using the latter here.
|
||||
|
||||
### reg-event-db
|
||||
|
||||
We register event handlers using re-frame's `reg-event-db`.
|
||||
We register event handlers using re-frame's `reg-event-db`:
|
||||
|
||||
`reg-event-db` is used like this:
|
||||
```clj
|
||||
(rf/reg-event-db
|
||||
:the-event-id
|
||||
the-event-handler-fn)
|
||||
```
|
||||
The handler function you provide should expect two parameters:
|
||||
- `db` the current application state (contents of `app-db`)
|
||||
- `v` the event vector
|
||||
The handler function you provide should expect two arguments:
|
||||
- `db`, the current application state (the value contained in `app-db`)
|
||||
- `v`, the event vector
|
||||
|
||||
So, your function will have a signature like this: `(fn [db v] ...)`.
|
||||
|
||||
Each event handler must compute and return the new state of
|
||||
the application, which means it normally returns a
|
||||
modified version of `db`.
|
||||
the application, which means it returns a
|
||||
modified version of `db` (or an unmodified one, if there are to be no changes to the state).
|
||||
|
||||
### :initialize
|
||||
|
||||
On startup, application state must be initialised. We
|
||||
want to put a sensible value into `app-db` which will
|
||||
otherwise contain `{}`.
|
||||
On startup, application state must be initialized. We
|
||||
want to put a sensible value into `app-db`, which starts out containing `{}`.
|
||||
|
||||
So a `(dispatch [:initialize])` will happen early in the
|
||||
apps life (more on this below), and we need to write an `event handler`
|
||||
app's life (more on this below), and we need to write an `event handler`
|
||||
for it.
|
||||
|
||||
Now this event handler is slightly unusual because it doesn't
|
||||
much care about the existing value in `db` - it just wants to plonk
|
||||
a completely new value.
|
||||
Now this event handler is slightly unusual because not only does it not care about
|
||||
any event information passed in via the `event` vector, but it doesn't
|
||||
even care about the existing value in `db` - it just wants to plonk
|
||||
a completely new value:
|
||||
|
||||
Like this:
|
||||
```clj
|
||||
(rf/reg-event-db ;; sets up initial application state
|
||||
:initialize
|
||||
:initialize
|
||||
(fn [_ _] ;; the two parameters are not important here, so use _
|
||||
{:time (js/Date.) ;; What it returns becomes the new application state
|
||||
:time-color "#f88"})) ;; so the application state will initially be a map with two keys
|
||||
|
@ -241,12 +237,12 @@ This particular handler `fn` ignores the two parameters
|
|||
a map literal, which becomes the application
|
||||
state.
|
||||
|
||||
Here's an alternative way of writing it which does pay attention to the existing value of `db`:
|
||||
For comparison, here's how we would have written this if we'd cared about the existing value of `db`:
|
||||
```clj
|
||||
(rf/reg-event-db
|
||||
:initialize
|
||||
(fn [db _] ;; we use db this time, so name it
|
||||
(-> db
|
||||
:initialize
|
||||
(fn [db _] ;; we use db this time, so name it
|
||||
(-> db
|
||||
(assoc :time (js/Date.))
|
||||
(assoc :time-color "#f88")))
|
||||
```
|
||||
|
@ -259,7 +255,7 @@ Earlier, we set up a timer function to `(dispatch [:timer now])` every second.
|
|||
Here's how we handle it:
|
||||
```clj
|
||||
(rf/reg-event-db ;; usage: (rf/dispatch [:timer a-js-Date])
|
||||
:timer
|
||||
:timer
|
||||
(fn [db [_ new-time]] ;; <-- de-structure the event vector
|
||||
(assoc db :time new-time))) ;; compute and return the new application state
|
||||
```
|
||||
|
@ -273,34 +269,34 @@ Notes:
|
|||
|
||||
When the user enters a new colour value (via an input text box):
|
||||
```clj
|
||||
(rf/reg-event-db
|
||||
(rf/reg-event-db
|
||||
:time-color-change ;; usage: (rf/dispatch [:time-color-change 34562])
|
||||
(fn [db [_ new-color-value]]
|
||||
(fn [db [_ new-color-value]]
|
||||
(assoc db :time-color new-color-value))) ;; compute and return the new application state
|
||||
```
|
||||
|
||||
## Effect Handlers (domino 3)
|
||||
|
||||
Domino 3 actions/realises the `effects` returned by event handlers.
|
||||
Domino 3 realises/puts into action the `effects` returned by event handlers.
|
||||
|
||||
In this "simple" application, our event handlers are implicitly returning
|
||||
only one effect: "update application state".
|
||||
|
||||
This particular `effect` is actioned by a re-frame supplied
|
||||
`effect handler`. **So, there's nothing for us to do for this domino**. We are
|
||||
This particular `effect` is accomplished by a re-frame-supplied
|
||||
`effect` handler. **So, there's nothing for us to do for this domino**. We are
|
||||
using a standard re-frame effect handler.
|
||||
|
||||
And this is not unusual. You'll seldom have to write `effect handlers`, but
|
||||
we'll understand more about them in a later tutorial.
|
||||
And this is not unusual. You'll seldom have to write `effect` handlers, but in a later
|
||||
tutorial we'll show you more about how to do so when you need to.
|
||||
|
||||
## Subscription Handlers (domino 4)
|
||||
|
||||
Subscription handlers take application state as an argument,
|
||||
and they compute a query over it, returning something of
|
||||
Subscription handlers, or `query` functions, take application state as an argument
|
||||
and run a query over it, returning something called
|
||||
a "materialised view" of that application state.
|
||||
|
||||
When the application state changes, subscription functions are
|
||||
re-run by re-frame, to compute new values (a new materialised view).
|
||||
re-run by re-frame, to compute new values (new materialised views).
|
||||
|
||||
Ultimately, the data returned by `query` functions is used
|
||||
in the `view` functions (Domino 5).
|
||||
|
@ -309,13 +305,13 @@ One subscription can
|
|||
source data from other subscriptions. So it is possible to
|
||||
create a tree of dependencies.
|
||||
|
||||
The Views (Domino 5) are the leaves of this tree The tree's
|
||||
The Views (Domino 5) are the leaves of this tree. The tree's
|
||||
root is `app-db` and the intermediate nodes between the two
|
||||
are computations being performed by the query functions of Domino 4.
|
||||
|
||||
Now, the two examples below are trivial. They just extract part of the application
|
||||
state and return it. So, there's virtually no computation. A more interesting tree
|
||||
of subscriptions and more explanation can be found in the todomvc example.
|
||||
of subscriptions, and more explanation, can be found in the todomvc example.
|
||||
|
||||
### reg-sub
|
||||
|
||||
|
@ -326,15 +322,15 @@ of subscriptions and more explanation can be found in the todomvc example.
|
|||
:some-query-id ;; query id (used later in subscribe)
|
||||
a-query-fn) ;; the function which will compute the query
|
||||
```
|
||||
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 the application state.
|
||||
Then later, a view function subscribes to a query like this:
|
||||
`(subscribe [:some-query-id])`, and `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)
|
||||
and that new value will be given to any view function which is subscribed
|
||||
to `:some-query-id`. This view function, itself, will then also be called again
|
||||
to compute new DOM (because it depends on a query value which changed).
|
||||
and that new value will be given to all `view` functions which are subscribed
|
||||
to `:some-query-id`. These `view` functions will then be called to compute the
|
||||
new DOM state (because the views depend on query results which have changed).
|
||||
|
||||
Along this reactive chain of dependencies, re-frame will ensure the
|
||||
necessary calls are made, at the right time.
|
||||
|
@ -358,7 +354,7 @@ Like I said, both of these queries are trivial. See [todomvc.subs.clj](https://g
|
|||
|
||||
`view` functions turn data into DOM. They are "State in, Hiccup out" and they are Reagent components.
|
||||
|
||||
Any SPA will have lots of `view`functions, and collectively,
|
||||
Any SPA will have lots of `view` functions, and collectively,
|
||||
they render the app's entire UI.
|
||||
|
||||
### Hiccup
|
||||
|
@ -366,7 +362,7 @@ they render the app's entire UI.
|
|||
`Hiccup` is a data format for representing HTML.
|
||||
|
||||
Here's a trivial view function which returns hiccup-formatted data:
|
||||
```clj
|
||||
```clj
|
||||
(defn greet
|
||||
[]
|
||||
[:div "Hello viewers"]) ;; means <div>Hello viewers</div>
|
||||
|
@ -383,11 +379,11 @@ And if we call it:
|
|||
|
||||
Yep, that's a vector with two elements: a keyword and a string.
|
||||
|
||||
Now,`greet` is pretty simple because it only has the "Hiccup Out" part. There's no "Data In".
|
||||
Now, `greet` is pretty simple because it only has the "Hiccup Out" part. There's no "Data In".
|
||||
|
||||
### Subscribing
|
||||
|
||||
To render the DOM representation of some-part-of app state, view functions must query
|
||||
To render the DOM representation of some part of the app state, view functions must query
|
||||
for that part of `app-db`, and that means using `subscribe`.
|
||||
|
||||
`subscribe` is always called like this:
|
||||
|
@ -396,7 +392,7 @@ for that part of `app-db`, and that means using `subscribe`.
|
|||
```
|
||||
There's only one (global) `subscribe` function and it takes one argument, assumed to be a vector.
|
||||
|
||||
The first element in the vector (shown above as `query-id`) identifies/names the query
|
||||
The first element in the vector (shown above as `query-id`) identifies the query,
|
||||
and the other elements are optional
|
||||
query parameters. With a traditional database a query might be:
|
||||
```
|
||||
|
@ -404,7 +400,7 @@ select * from customers where name="blah"
|
|||
```
|
||||
|
||||
In re-frame, that would be done as follows:
|
||||
`(subscribe [:customer-query "blah"])`
|
||||
`(subscribe [:customer-query "blah"])`,
|
||||
which would return a `ratom` holding the customer state (a value which might change over time!).
|
||||
|
||||
> Because subscriptions return a ratom, they must always be dereferenced to
|
||||
|
@ -439,10 +435,9 @@ And this view function renders the input field:
|
|||
|
||||
Notice how it does BOTH a `subscribe` to obtain the current value AND a `dispatch` to say when it has changed.
|
||||
|
||||
It is very common for view functions to render event-dispatching functions. The user's interaction with
|
||||
the UI is usually the largest source of events.
|
||||
It is very common for view functions to run event-dispatching functions. The user's interaction with the UI is usually the largest source of events.
|
||||
|
||||
And then something more standard:
|
||||
And then a `view` function to bring the others together, which contains no subscriptions or dispatching of its own:
|
||||
```clj
|
||||
(defn ui
|
||||
[]
|
||||
|
@ -467,17 +462,17 @@ Django, Rails, Handlebars or Mustache -- they map data to HTML -- except for two
|
|||
downside is that these are not "designer friendly" HTML templates.
|
||||
2. these templates are reactive. When their input Signals change, they
|
||||
are automatically rerun, producing new 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.
|
||||
the renderer of any `component` is wrapped by a `reaction`. If any of the "inputs"
|
||||
to that renderer change, the renderer is rerun.
|
||||
|
||||
## Kick Starting The App
|
||||
|
||||
Below, `run` is the called when the HTML page has loaded
|
||||
to kick off the application.
|
||||
Below, `run` is the called to kick off the application once the HTML page has loaded.
|
||||
|
||||
It has two tasks:
|
||||
1. load the initial application state
|
||||
2. "mount" the GUI onto an existing DOM element.
|
||||
1. Load the initial application state
|
||||
2. Load the GUI by "mounting" the root-level function in the hierarchy of `view` functions -- in our case, `ui` --
|
||||
onto an existing DOM element.
|
||||
|
||||
```clj
|
||||
(defn ^:export run
|
||||
|
@ -487,19 +482,18 @@ It has two tasks:
|
|||
(js/document.getElementById "app")))
|
||||
```
|
||||
|
||||
After `run` is called, the app passively waits for events.
|
||||
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 is something of
|
||||
cheat which ensures a correct
|
||||
notice the use of `dispatch-sync`, rather than `dispatch`. This is a cheat which ensures that a correct
|
||||
structure exists in `app-db` before any subscriptions or event handlers run.
|
||||
|
||||
## Summary
|
||||
|
||||
**Your job**, when building an app, is to:
|
||||
- design your app's information model (data and schema layer)
|
||||
- write and register event handler functions (control and transition layer) (domino 2)
|
||||
- write and register event handler functions (control and transition layer) (domino 2)
|
||||
- (once in a blue moon) write and register effect and coeffect handler
|
||||
functions (domino 3) which do the mutative dirty work of which we dare not
|
||||
speak in a pure, immutable functional context. Most of the time, you'll be
|
||||
|
@ -509,7 +503,7 @@ structure exists in `app-db` before any subscriptions or event handlers run.
|
|||
|
||||
## Next Steps
|
||||
|
||||
You should now take time to carefully review the [todomvc example application](https://github.com/Day8/re-frame/tree/develop/examples/todomvc).
|
||||
You should now take time to carefully review the [todomvc example application](https://github.com/Day8/re-frame/tree/develop/examples/todomvc).
|
||||
|
||||
After that, you'll be ready to write your own code. Perhaps you will use a
|
||||
template to create your own project: <br>
|
||||
|
|
|
@ -65,7 +65,7 @@ with a function to do the handling:
|
|||
```clj
|
||||
(re-frame.core/reg-event-db ;; <-- call this to register a handler
|
||||
:set-flag ;; this is an event id
|
||||
(fn [db [_ new-value] ;; this function does the handling
|
||||
(fn [db [_ new-value]] ;; this function does the handling
|
||||
(assoc db :flag new-value)))
|
||||
```
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ But **there are other perspectives** on re-frame
|
|||
which will deepen your understanding.
|
||||
|
||||
This tutorial is a tour of these ideas, justifications and insights.
|
||||
It is a little rambling, but I'm hoping it will deliver for you
|
||||
It is a little rambling, but I'm hoping it will deliver for you
|
||||
at least one "Aaaah, I see" moment before the end.
|
||||
|
||||
> If a factory is torn down but the rationality which produced it is
|
||||
|
@ -53,10 +53,10 @@ For all its considerable brilliance, Reagent (+ React)
|
|||
delivers only the 'V' part of a traditional MVC framework.
|
||||
|
||||
But apps involve much more than V. We build quite complicated
|
||||
SPAs which can run to 50K lines of code. So, I wanted to know:
|
||||
SPAs which can run to 50K lines of code. So, I wanted to know:
|
||||
where does the control logic go? How is state stored & manipulated? etc.
|
||||
|
||||
We read up on [Pedestal App], [Flux],
|
||||
We read up on [Pedestal App], [Flux],
|
||||
[Hoplon], [Om], early [Elm], etc., and re-frame is the architecture that
|
||||
emerged. Since then, we've tried to keep an eye on further developments like the
|
||||
Elm Architecture, Om.Next, BEST, Cycle.js, Redux, etc. They have taught us much
|
||||
|
@ -86,18 +86,18 @@ a system out of pure functions.
|
|||
__Third__, we believe in the primacy of data, for the reasons described in
|
||||
the main README. re-frame has a data oriented, functional architecture.
|
||||
|
||||
__Fourth__, we believe that Reactive Programming is one honking good idea.
|
||||
How did we ever live without it? It is a quite beautiful solution to one half of re-frame's
|
||||
data conveyance needs, **but** we're cautious about taking it too far - as far as, say, cycle.js.
|
||||
__Fourth__, we believe that Reactive Programming is one honking good idea.
|
||||
How did we ever live without it? It is a quite beautiful solution to one half of re-frame's
|
||||
data conveyance needs, **but** we're cautious about taking it too far - as far as, say, cycle.js.
|
||||
It doesn't take over everything in re-frame - it just does part of the job.
|
||||
|
||||
__Finally__, a long time ago in a galaxy far far away, I was lucky enough to program in Eiffel
|
||||
__Finally__, a long time ago in a galaxy far far away, I was lucky enough to program in Eiffel
|
||||
where I was exposed to the idea of [command-query separation](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation).
|
||||
The modern rendering of this idea is CQRS ([see resources here](http://www.baeldung.com/cqrs-event-sourced-architecture-resources)).
|
||||
But, even today, we still see read/write `cursors` and two-way data binding being promoted as a good thing.
|
||||
Please, just say no. We already know where that goes. As your programs get bigger, the use of these two-way constructs
|
||||
will encourage control logic into all the
|
||||
wrong places and you'll end up with a tire-fire of an Architecture. <br>
|
||||
The modern rendering of this idea is CQRS ([see resources here](http://www.baeldung.com/cqrs-event-sourced-architecture-resources)).
|
||||
But, even today, we still see read/write `cursors` and two-way data binding being promoted as a good thing.
|
||||
Please, just say no. We already know where that goes. As your programs get bigger, the use of these two-way constructs
|
||||
will encourage control logic into all the
|
||||
wrong places and you'll end up with a tire-fire of an Architecture. <br>
|
||||
Sincerely, The Self-appointed President of the Cursor Skeptic's Society.
|
||||
|
||||
## It does Event Sourcing
|
||||
|
@ -119,7 +119,7 @@ Note: that's all just data. **Pure, lovely loggable data.**
|
|||
|
||||
If you have that data, then you can reproduce the error.
|
||||
|
||||
re-frame allows you to time travel, even in a production setting.
|
||||
re-frame allows you to time travel, even in a production setting.
|
||||
Install the "checkpoint" state into `app-db`
|
||||
and then "play forward" through the collection of dispatched events.
|
||||
|
||||
|
@ -131,22 +131,22 @@ a checkpoint of `app-db`, and the events since then.
|
|||
|
||||
Here's Martin Fowler's [description of Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html).
|
||||
|
||||
## It does a reduce
|
||||
## It does a reduce
|
||||
|
||||
|
||||
Here's an interesting way of thinking about the re-frame
|
||||
Here's an interesting way of thinking about the re-frame
|
||||
data flow ...
|
||||
|
||||
**First**, imagine that all the events ever dispatched in a
|
||||
**First**, imagine that all the events ever dispatched in a
|
||||
certain running app were stored in a collection (yes, event sourcing again).
|
||||
So, if when the app started, the user clicked on button X
|
||||
the first item in this collection would be the event
|
||||
generated by that button, and then, if next the user moved
|
||||
a slider, the associated event would be the next item in
|
||||
So, if when the app started, the user clicked on button X
|
||||
the first item in this collection would be the event
|
||||
generated by that button, and then, if next the user moved
|
||||
a slider, the associated event would be the next item in
|
||||
the collection, and so on and so on. We'd end up with a
|
||||
collection of event vectors.
|
||||
|
||||
**Second**, remind yourself that the `combining function`
|
||||
**Second**, remind yourself that the `combining function`
|
||||
of a `reduce` takes two arguments:
|
||||
1. the current state of the reduction and
|
||||
2. the next collection member to fold in
|
||||
|
@ -157,48 +157,48 @@ Then notice that `reg-event-db` event handlers take two arguments also:
|
|||
|
||||
Interesting. That's the same as a `combining function` in a `reduce`!!
|
||||
|
||||
So now we can introduce the new mental model: at any point in time,
|
||||
So now we can introduce the new mental model: at any point in time,
|
||||
the value in `app-db` is the result of performing a `reduce` over
|
||||
the entire `collection` of events dispatched in the app up until
|
||||
the entire `collection` of events dispatched in the app up until
|
||||
that time. The combining function for this reduce is the set of event handlers.
|
||||
|
||||
It is almost like `app-db` is the temporary place where this
|
||||
It is almost like `app-db` is the temporary place where this
|
||||
imagined `perpetual reduce` stores its on-going reduction.
|
||||
|
||||
Now, in the general case, this perspective breaks down a bit,
|
||||
because of `reg-event-fx` (has `-fx` on the end, not `-db`) which
|
||||
Now, in the general case, this perspective breaks down a bit,
|
||||
because of `reg-event-fx` (has `-fx` on the end, not `-db`) which
|
||||
allows:
|
||||
1. Event handlers to produce `effects` beyond just application state
|
||||
1. Event handlers to produce `effects` beyond just application state
|
||||
changes.
|
||||
2. Event handlers to have `coeffects` (arguments) in addition to `db` and `v`.
|
||||
2. Event handlers to have `coeffects` (arguments) in addition to `db` and `v`.
|
||||
|
||||
But, even if it isn't the full picture, it is a very useful
|
||||
But, even if it isn't the full picture, it is a very useful
|
||||
and interesting mental model. We were first exposed to this idea
|
||||
via Elm's early use of `foldp` (fold from the past), which was later enshrined in the
|
||||
via Elm's early use of `foldp` (fold from the past), which was later enshrined in the
|
||||
Elm Architecture.
|
||||
|
||||
## Derived Data All The Way Down
|
||||
|
||||
For the love of all that is good, please watch this terrific
|
||||
[StrangeLoop presentation](https://www.youtube.com/watch?v=fU9hR3kiOK0) (40 mins).
|
||||
See what happens when you re-imagine a database as a stream!! Look at
|
||||
[StrangeLoop presentation](https://www.youtube.com/watch?v=fU9hR3kiOK0) (40 mins).
|
||||
See what happens when you re-imagine a database as a stream!! Look at
|
||||
all the problems that evaporate.
|
||||
Think about that: shared mutable state (the root of all evil),
|
||||
re-imagined as a stream!! Blew my socks off.
|
||||
|
||||
If, by chance, you ever watched that video (you should!), you might then twig to
|
||||
the idea that `app-db` is really a derived value ... the video talks
|
||||
If, by chance, you ever watched that video (you should!), you might then twig to
|
||||
the idea that `app-db` is really a derived value ... the video talks
|
||||
a lot about derived values. So, yes, app-db is a derived value of the `perpetual reduce`.
|
||||
|
||||
And yet, it acts as the authoritative source of state in the app. And yet,
|
||||
And yet, it acts as the authoritative source of state in the app. And yet,
|
||||
it isn't, it is simply a piece of derived state. And yet, it is the source. Etc.
|
||||
|
||||
This is an infinite loop of sorts - an infinite loop of derived data.
|
||||
|
||||
## It does FSM
|
||||
|
||||
> Any sufficiently complicated GUI contains an ad hoc,
|
||||
> informally-specified, bug-ridden, slow implementation
|
||||
> Any sufficiently complicated GUI contains an ad hoc,
|
||||
> informally-specified, bug-ridden, slow implementation
|
||||
> of a hierarchical Finite State Machine <br>
|
||||
> -- me, trying too hard to impress my two twitter followers
|
||||
|
||||
|
@ -213,19 +213,19 @@ moves from one logical state to the next.
|
|||
|
||||
In the simplest
|
||||
case, `app-db` will contain a single value which represents the current "logical state".
|
||||
For example, there might be a single `:phase` key which can have values like `:loading`,
|
||||
`:not-authenticated` `:waiting`, etc. Or, the "logical state" could be a function
|
||||
of many values in `app-db`.
|
||||
For example, there might be a single `:phase` key which can have values like `:loading`,
|
||||
`:not-authenticated` `:waiting`, etc. Or, the "logical state" could be a function
|
||||
of many values in `app-db`.
|
||||
|
||||
Not every app has lots of logical states, but some do, and if you are implementing
|
||||
one of them, then formally recognising it and using a technique like
|
||||
Not every app has lots of logical states, but some do, and if you are implementing
|
||||
one of them, then formally recognising it and using a technique like
|
||||
[State Charts](https://www.amazon.com/Constructing-User-Interface-Statecharts-Horrocks/dp/0201342782)
|
||||
will help greatly in getting a clean design and fewer bugs.
|
||||
|
||||
The beauty of re-frame, from a FSM point of view, is that all the state is
|
||||
in one place - unlike OO systems where the state is distributed (and synchronized)
|
||||
The beauty of re-frame, from a FSM point of view, is that all the state is
|
||||
in one place - unlike OO systems where the state is distributed (and synchronised)
|
||||
across many objects. So implementing your control logic as a FSM is
|
||||
fairly natural in re-frame, whereas it is often difficult and
|
||||
fairly natural in re-frame, whereas it is often difficult and
|
||||
contrived in other kinds of architecture (in my experience).
|
||||
|
||||
So, members of the jury, I put it to you that:
|
||||
|
@ -233,59 +233,59 @@ So, members of the jury, I put it to you that:
|
|||
- the last 3 dominoes render of the FSM's current state for the user to observe
|
||||
|
||||
Depending on your app, this may or may not be a useful mental model,
|
||||
but one thing is for sure ...
|
||||
but one thing is for sure ...
|
||||
|
||||
Events - that's the way we roll.
|
||||
|
||||
|
||||
## Interconnections
|
||||
|
||||
Ask a Systems Theorist, and they'll tell you that a system has **parts** and **interconnections**.
|
||||
Ask a Systems Theorist, and they'll tell you that a system has **parts** and **interconnections**.
|
||||
|
||||
Human brains tend to focus first on the **parts**, and then, later, maybe on
|
||||
**interconnections**. But we know better, right? We
|
||||
**interconnections**. But we know better, right? We
|
||||
know interconnections are often critical to a system.
|
||||
"Focus on the lines between the boxes" we lecture anyone kind enough to listen (in my case, glassy-eyed family members).
|
||||
|
||||
In the case of re-frame, dominoes are the **parts**, so, tick, yes, we have
|
||||
looked at them first. Our brains are happy. But what about the **interconnections**?
|
||||
|
||||
If the **parts** are functions, as is the case with re-frame,
|
||||
what does it even mean to talk about **interconnections between functions?**
|
||||
If the **parts** are functions, as is the case with re-frame,
|
||||
what does it even mean to talk about **interconnections between functions?**
|
||||
To answer that question, I'll rephrase it as:
|
||||
how are the domino functions **composed**?
|
||||
|
||||
At the language level,
|
||||
At the language level,
|
||||
Uncle Alonzo and Uncle John tell us how a function like `count` composes:
|
||||
```clj
|
||||
(str (count (filter odd? [1 2 3 4 5])))
|
||||
```
|
||||
We know when `count` is called, and with what
|
||||
argument, and how the value it computes becomes the arg for a further function.
|
||||
We know when `count` is called, and with what
|
||||
argument, and how the value it computes becomes the arg for a further function.
|
||||
We know how data "flows" into and out of the functions.
|
||||
|
||||
Sometimes, we'd rewrite this code as:
|
||||
Sometimes, we'd rewrite this code as:
|
||||
```clj
|
||||
(->> [1 2 3 4 5]
|
||||
(filter odd?)
|
||||
count
|
||||
str)
|
||||
```
|
||||
With this arrangement, we talk of "threading" data
|
||||
through functions. **It seems to help our comprehension to frame function
|
||||
With this arrangement, we talk of "threading" data
|
||||
through functions. **It seems to help our comprehension to frame function
|
||||
composition in terms of data flow**.
|
||||
|
||||
re-frame delivers architecture
|
||||
by supplying the interconnections - it threads the data - it composes the dominoes - it is the lines between the boxes.
|
||||
by supplying the interconnections - it threads the data - it composes the dominoes - it is the lines between the boxes.
|
||||
|
||||
But it doesn't have a universal method for this "composition". The technique it uses varies from one domino
|
||||
neighbour-pair to the next. Initially, it uses a queue/router, then a pipeline of interceptors
|
||||
But it doesn't have a universal method for this "composition". The technique it uses varies from one domino
|
||||
neighbour-pair to the next. Initially, it uses a queue/router, then a pipeline of interceptors
|
||||
and, finally, a Signal Graph.
|
||||
|
||||
Remember back in the original README? Our analogy for re-frame was the water cycle - water flowing around the loop,
|
||||
Remember back in the original README? Our analogy for re-frame was the water cycle - water flowing around the loop,
|
||||
compelled by different kinds of forces at different times (gravity, convection, etc), going through phase changes.
|
||||
|
||||
With this focus on interconnections, we have been looking on the "forces" part of the loop. The transport.
|
||||
|
||||
With this focus on interconnections, we have been looking on the "forces" part of the loop. The transport.
|
||||
|
||||
|
||||
## Full Stack
|
||||
|
@ -294,7 +294,7 @@ If you like re-frame and want to take the principles full-stack, then
|
|||
these resources might be interesting to you:
|
||||
|
||||
Commander Pattern
|
||||
https://www.youtube.com/watch?v=B1-gS0oEtYc
|
||||
https://www.youtube.com/watch?v=B1-gS0oEtYc
|
||||
|
||||
Datalog All The Way Down
|
||||
https://www.youtube.com/watch?v=aI0zVzzoK_E
|
||||
|
@ -318,7 +318,7 @@ Of course, that only applies if you get passionate about a technology
|
|||
But, no. No! Those French Philosophers and their pessimism - ignore him!!
|
||||
Your love for re-frame will be deep, abiding and enriching.
|
||||
|
||||
***
|
||||
***
|
||||
|
||||
Previous: [First Code Walk-Through](CodeWalkthrough.md)
|
||||
Up: [Index](README.md)
|
||||
|
|
|
@ -5,23 +5,23 @@ This tiny application is meant to provide a quick start of the basics of re-fram
|
|||
A detailed source code walk-through is provided in the docs:
|
||||
https://github.com/Day8/re-frame/blob/master/docs/CodeWalkthrough.md
|
||||
|
||||
All the code is in one namespace `/src/simpleexample/core.cljs`
|
||||
All the code is in one namespace: `/src/simpleexample/core.cljs`
|
||||
|
||||
### Run It And Change It
|
||||
|
||||
Steps:
|
||||
|
||||
A. Check out the re-frame repo
|
||||
1. Get a command line
|
||||
2. `cd` to the root of this sub project (where this README exists)
|
||||
3. run "`lein do clean, figwheel`" to compile the app,
|
||||
4. open `http://localhost:3449/example.html` to see the app
|
||||
1. Check out the re-frame repo
|
||||
2. Get a command line
|
||||
3. `cd` to the root of this sub project (where this README exists)
|
||||
4. run "`lein do clean, figwheel`" to compile the app and start up figwheel hot-reloading,
|
||||
5. open `http://localhost:3449/example.html` to see the app
|
||||
|
||||
Whileever step 3 is running, any changes you make to the ClojureScript
|
||||
While step 4 is running, any changes you make to the ClojureScript
|
||||
source files (in `src`) will be re-compiled and reflected in the running
|
||||
page immediately.
|
||||
|
||||
### Production Version
|
||||
|
||||
Run "`lein do clean, with-profile prod compile`" to compile an optimized
|
||||
version, and then open `resources/public/example.html`.
|
||||
Run "`lein do clean, with-profile prod compile`" to compile an optimised
|
||||
version, and then open `resources/public/example.html` in a browser.
|
||||
|
|
|
@ -29,7 +29,7 @@ A [re-frame](https://github.com/Day8/re-frame) implementation of [TodoMVC](http:
|
|||
```
|
||||
|
||||
|
||||
## Compile an optimized version
|
||||
## Compile an optimised version
|
||||
|
||||
1. Compile
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue