Live
Forget overviews from 60,000 feet or 30,000 feet. Now we're at 0 feet.
We're writing code for a real app - live, in this document.
Which App?¶
The "simple" one.
Within the re-frame repository's /examples
folder is an app
called /simple.
It has 70 lines of code in a single namespace. Below, we'll look at all 70 lines.
Below, you should see the app running live.
Try changing the display colour to magenta
, #f00
or #02a6f2
.
Doesn't work? Maybe try disabling your adblocker for this site.
When you change the live code on this page, the app will change. This live coding is powered by SCI.
The Namespace¶
Within our single namespace (called re-frame.simple
), we'll need to use both reagent
and re-frame
.
So, at the top, within the ns
, we'll give the name and require the other namespaces we'll need:
Live Code Fragment
Above, the code is provided within an editor. You can change the code if you want, and then press the "eval" button.
Below the editor, you'll see a green-backgrounded box. If the code was evaluated successfully a tick is shown along with the value of that evaluation.
In the case of our ns
form, it evaluates to nil
, which is not very interesting.
The Data Schema¶
Let's talk app-db
. Now, usually, I'd recommend that you write a schema
for your application state, but here, we'll cut that corner to minimise cognitive load.
But we can't cut it completely. You'll still need an
informal description, and here it is ... app-db
will contain
a two-key map like this:
{:time (js/Date.) ;; current time for display
:time-color "#f88"} ;; the colour in which the time should be shown
Event Dispatch¶
Events are data.
re-frame uses a vector format for events. For example:
[:time-color-change "red"]
The first element in the vector is a keyword that identifies the kind
of event
.
Further elements are optional and can provide additional data
associated with the event. The additional value above, "red"
, is
presumably the colour.
Here are some other example events:
[:admit-to-being-satoshi false]
[:dressing/put-pants-on "velour flares" {:method :left-leg-first :belt false}]
For non-trivial applications, the kind
keyword will be namespaced, as it is in the 2nd example.
dispatch¶
To send an event, call dispatch
with the event vector as the argument.
(dispatch [:kind-of-event value1 value2])
For our simple app, we do this ...
Notes:
defn
returns nothing of interest, you can ignore the green box below the code.- the current time is obtained with
(js/Date.)
which is the equivalent ofnew Date()
in javascript. - Within the
ns
at the top of the namespace,re-frame
is aliased asrf
via[re-frame.core :as rf]
. So, the symbolrf/dispatch
is a reference to the functiondispatch
in the re-frame API.
Notes:
setInterval
is used to calldispatch-timer-event
every seconddefonce
is likedef
, it instantiates a symbol and binds a value to it. But the evaluation won't happen if the symbol (do-timer
in this case) already exists. This stops a new timer from getting created every time we hot-reload the code in a namespace. This would be important in a dev environment where we were editing the namespace and our changes were causing it to be recompiled and hot-code reloaded.
A timer is an unusual source of events. Usually, it is an app's UI widgets that dispatch events
(in response to user actions), or an HTTP POST's on-success
handler or a websocket which gets a new packet.
So, this "simple" app is unusual. Moving on.
After dispatch¶
dispatch
puts an event onto a queue for processing.
So, an event is not processed synchronously, like a function call. The processing happens later - asynchronously. Very soon, but not now.
The consumer of the queue is the re-frame router
which looks after the event's processing.
The router
will:
- inspect the 1st element of an event vector
- look up the event handler (function) registered for this kind of event
- call this event handler (function) with the necessary arguments
Our job, then, is to register an event handler function for
each kind of event, including this :timer
event.
Event Handlers¶
Collectively, event handlers provide the control logic in a re-frame application.
In this application, three kinds of event are dispatched:
:initialize
- dispatch once, when the program boots up:time-color-change
- dispatched whenever the user changes the colour text field:timer
- dispatched once a second via a timer
Having 3 events means we'll be registering 3 event handlers.
Registering Event Handlers¶
Event handler functions:
- take two arguments
coeffects
andevent
- return
effects
Conceptually, you can think of the argument coeffects
as being "the current state of the world".
And you can think of event handlers as computing how the world should be changed
by the arriving event. They return (as data) how the world should be changed by the event - the side effects
of the event.
Event handlers can be registered in two ways:
reg-event-fx
reg-event-db
One ends in -fx
and the other in -db
.
reg-event-fx
can take manycoeffects
and can return manyeffects
reg-event-db
allows you to write simpler handlers for the common case where you want them to take only onecoeffect
- the current app state - and return oneeffect
- the updated app state.
Many vs One. Because of its simplicity, we'll be using the latter one here: reg-event-db
.
reg-event-db¶
We register event handlers using re-frame's API:
(rf/reg-event-db ;; <-- the re-frame API function to use
:the-event-id ;; <-- the event id
the-event-handler-fn) ;; <-- the handler function
The handler function you provide should expect two arguments:
db
- the current application state (the map value contained inapp-db
)e
- the event vector (given todispatch
)
So, your handler function will have a signature like this: (fn [db e] ...)
.
These event handlers must compute and return the new state of
the application, which means they return a modified version of db
.
:timer¶
Notes:
- the
event
(2nd parameter) will be like[:timer a-js-time]
- sequence destructuring
is used to extract the 2nd element of that event vector (ignores first with
_
) db
is a map, containing two keys (see description above)- this handler computes a new application state from
db
, and returns it - it just
assocs
in thenew-time
value
:initialize¶
Once on startup, application state must be initialised. We
want to put a sensible value into app-db
, which starts out containing an empty map {}
.
A single (dispatch [:initialize])
will happen early in the
app's life (more on this below), and we need to register an event handler
for it.
This event handler is slightly unusual because it ignores both of its arguments.
There's nothing in the event
vector which it needs. Nor is the existing value in
db
. It just wants to plonk a completely new value into app-db
For comparison, here's how we could have written this if we did care about the existing value in db
:
(rf/reg-event-db
:initialize
(fn [db _] ;; we use db this time, so name it
(-> db ;; db is initially just {}
(assoc :time (js/Date.))
(assoc :time-color "orange")))
:time-color-change¶
When the user enters a new colour value (into the input field) the view will (dispatch [:time-color-change new-colour])
(more on this below).
Notes:
- it updates
db
to contain the new colour, provided as the 2nd element of the event
Effect Handlers¶
Domino 3 actions 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 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 seldom write effect
handlers. But it is covered in
a later tutorial.
Subscription Handlers¶
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 (new materialised views).
Ultimately, the data returned by these query
functions flow through into
the view
functions (Domino 5).
One subscription can source data from other subscriptions. So it is possible to create a tree structure of data flow.
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 subscriptions below are trivial. They just extract part of the application
state and return it. So, there's virtually no computation - the materialised view they
produce is the same as that stored in app-db
. A more interesting tree
of subscriptions, and more explanation, can be found in the todomvc example.
reg-sub¶
reg-sub
associates a query id
with a function that computes
that query, like this:
(rf/reg-sub
:some-query-id ;; query id (used later in subscribe)
a-query-fn) ;; the function which will compute the query
Later, a view function (domino 5) will subscribe 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-db
)
and that new value will be given to all view
functions that 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 that have changed).
Along this reactive chain of dependencies, re-frame will ensure the necessary calls are made, at the right time.
Here's the code for defining our 2 subscription handlers:
Both of these queries are trivial. They are known as "accessors", or layer 2, subscriptions. More on that soon.
View Functions¶
view
functions compute Hiccup. They are "Data in, Hiccup out" and they are Reagent
components.
A SPA will have lots of view
functions, and collectively,
they render the app's UI.
Subscribing¶
To render a hiccup representation of some part of the app state, view functions must query
for that part of app-db
, by using subscribe
.
subscribe
is used like this:
(rf/subscribe [query-id some optional query parameters])
So subscribe
takes one argument, assumed to be a vector.
The first element in the vector identifies the query, and the other elements are optional query parameters. With a traditional database, a query might be:
select * from customers where name="blah"
In re-frame, that would look like:
(subscribe [:customer-query "blah"])
,
which would return a ratom
holding the customer state (a value that might change over time!).
Rookie Mistake
Because subscriptions return a ratom
(a Reagent atom), they must always be dereferenced to
obtain the value (using deref or the reader macro @
). Forgetting to do this is a recurring papercut for newbies.
The View Functions¶
This view function renders the clock:
As you can see, it uses subscribe
twice to obtain two pieces of data from app-db
.
If either value changes, Reagent will automatically re-run this view function,
computing new hiccup, which means new DOM.
Using the power of sci
, we can render just the clock
component:
When an event handler changes a value in app-db
, clock
will rerender. Try it.
Edit the following code to remove the comment, and then press the Eval button to execute it. Notice the change in the clock above. Experiment.
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 (look for emit
).
It is very common for view functions to render event-dispatching functions into the DOM. The user's interaction with the UI is usually a large source of events.
Notice also how we use @
in front of subscribe
to obtain the value out of the subscription. It is almost as if the subscription is an atom holding a value (which can change over time).
We can render the color-input
as any other reagent component:
(rdc/render color-input-root [color-input])
And then there's a parent view
to arrange the others. It contains no
subscriptions or dispatching of its own:
view
functions form a hierarchy, often with
data flowing from parent to child via
arguments (props in React). So, not every view needs a subscription if the
values passed in from a parent component are sufficient.
view
functions never directly access app-db
. Data is
only ever sourced via subscriptions.
Kick Starting The App¶
Below, the function run
is called to kick off the application once the HTML page has loaded.
It has two tasks:
- Load the initial application state
- Load the GUI by "mounting" the root-level
view
- in our case,ui
- onto an existing DOM element (with idapp
).
When it comes to establishing initial application state, you'll
notice the use of dispatch-sync
, rather than dispatch
. This is a simplifying cheat
which ensures that a correct
structure exists in app-db
before any subscriptions or event handlers run.
After run
is called, the app passively waits for events
.
Nothing happens without an event
.
The run function renders the app in the DOM element whose id is dominoes-live-app
: this DOM element is located at the top of the page.
This is the element we used to show how the app looks at the top of this page
To save you the trouble of scrolling up to the top of the page, I decided to render the whole app as a reagent element, just here:
T-Shirt Unlocked¶
Good news. If you've read this far, your insider's T-shirt will be arriving soon - it will feature turtles, xkcd and something about "data all the way down". But we're still working on the hilarious caption bit. Open a repo issue with a suggestion.