One pass over loading-initial-data docs

This commit is contained in:
Mike Thompson 2016-08-25 19:24:00 +10:00
parent 06ddd2e852
commit 6d11a0116d
1 changed files with 70 additions and 35 deletions

View File

@ -1,7 +1,11 @@
## Bootstrapping Your Application State ## Bootstrapping Your Application State
To bootstrap a re-frame application, you need to: To bootstrap a re-frame application, you need to:
1. register handlers (subscription and event handlers) 1. register handlers
- subscription (via `reg-sub`)
- events (via `reg-event-db` or `reg-event-fx`)
- effects (via `reg-fx`)
- coeffects (via `reg-cofx`)
2. kickstart reagent (views) 2. kickstart reagent (views)
3. Load the right initial data into `app-db` which might be a `merge` of: 3. Load the right initial data into `app-db` which might be a `merge` of:
- Some default values - Some default values
@ -11,23 +15,24 @@ To bootstrap a re-frame application, you need to:
Point 3 is the interesting bit and will be the main focus of this page, but let's work our way through them ... Point 3 is the interesting bit and will be the main focus of this page, but let's work our way through them ...
## Register Event Handlers ## 1. Register Handlers
Generally, there's nothing to do because this happens automatically at (js) script load time, because you declared and registered your event handlers like this: You declare and registered your handlers in the one step like this:
```clj
```Clojure
(re-frame/reg-event-db ;; event handler will be registered automatically (re-frame/reg-event-db ;; event handler will be registered automatically
:some-id :some-id
(fn [db [_ value]] (fn [db [_ value]]
... do some state change based on db and value ... do some state change based on db and value ))
``` ```
## Kick Start Reagent As a result, there's nothing further you need to do because, at script (js) load time, the
registration happens automatically.
## 2. Kick Start Reagent
Create a function `main` which does a `reagent/render` of your root reagent component `main-panel`: Create a function `main` which does a `reagent/render` of your root reagent component `main-panel`:
```Clojure ```clj
(defn main-panel ;; my top level reagent component (defn main-panel ;; my top level reagent component
[] []
[:div "Hello DDATWD"]) [:div "Hello DDATWD"])
@ -38,17 +43,24 @@ Create a function `main` which does a `reagent/render` of your root reagent comp
(js/document.getElementById "app"))) (js/document.getElementById "app")))
``` ```
## Loading Initial Data Mounting the top level component `main-panel` will trigger a cascade of child
component creation. The full DOM tree will be rendered.
Let's rewrite our `main-panel` component to use a subscription: ## 3. Loading Initial Data
Let's rewrite our `main-panel` component to use a subscription. In effect,
we want it to source and render some data held in `app-db`.
First, we'll create the subscription handler:
```Clojure ```Clojure
(re-frame/reg-sub ;; a new subscription handler (re-frame/reg-sub ;; a new subscription handler
:name ;; usage (subscribe [:name]) :name ;; usage (subscribe [:name])
(fn [db _] (fn [db _]
(:name db))) ;; pulls out :name (:display-name db))) ;; extracts `:display-name` from app-db
```
And now we use that subscription:
```clj
(defn main-panel (defn main-panel
[] []
(let [name (re-frame/subscribe [:name])] ;; <--- a subscription <--- (let [name (re-frame/subscribe [:name])] ;; <--- a subscription <---
@ -57,31 +69,31 @@ Let's rewrite our `main-panel` component to use a subscription:
``` ```
The user of our app will see funny things The user of our app will see funny things
if that `(subscribe [:name])` doesn't deliver good data. We must ensure there's good data in `app-db`. if that `(subscribe [:name])` doesn't deliver good data. But how do we ensure "good data"?
That will require: That will require:
1. getting data into `app-db`; and 1. getting data into `app-db`; and
2. not get into trouble if that data isn't yet in `app-db`. For example, the data may have to come from a server and there's latency. 2. not get into trouble if that data isn't yet in `app-db`. For example,
the data may have to come from a server and there's latency.
**Note: `app-db` initially contains `{}`** **Note: `app-db` initially contains `{}`**
### Getting Data Into `app-db` ### Getting Data Into `app-db`
Only event handlers can change `app-db`. Those are the rules!! Even initial values must be put in via an event handler. Only event handlers can change `app-db`. Those are the rules!! Even initial
values must be put in via an event handler.
Here's an event handler for that purpose: Here's an event handler for that purpose:
```Clojure ```Clojure
(re-frame/reg-event-db (re-frame/reg-event-db
:initialise-db ;; usage: (re-frame/dispatch [:initialise-db]) :initialise-db ;; usage: (dispatch [:initialise-db])
(fn [_ _] ;; Ignore both params (db and event) (fn [_ _] ;; Ignore both params (db and event)
{:display-name "DDATWD" ;; return a new value for app-db {:display-name "DDATWD" ;; return a new value for app-db
:items [1 2 3 4]})) :items [1 2 3 4]}))
``` ```
We'll need to dispatch the `:initialise-db` event to get it to execute. `main` seems like the natural place: We'll need to dispatch an `:initialise-db` event to get it to execute. `main` seems like the natural place:
```Clojure ```Clojure
(defn ^:export main (defn ^:export main
[] []
@ -90,13 +102,19 @@ We'll need to dispatch the `:initialise-db` event to get it to execute. `main` s
(js/document.getElementById "app"))) (js/document.getElementById "app")))
``` ```
But remember, event handlers execute async. So although there's a `dispatch` within `main`, the handler for `:initialise-db` will not be run until sometime after `main` has finished. But remember, event handlers execute async. So although there's
a `dispatch` within `main`, the handler for `:initialise-db`
will not be run until sometime after `main` has finished.
But how long after? And is there a race condition? The component `main-panel` (which needs good data) might be rendered before the `:initialise-db` event handler has put good data into `app-db`. But how long after? And is there a race condition? The
component `main-panel` (which needs good data) might be
rendered before the `:initialise-db` event handler has
put good data into `app-db`.
We don't want any rendering (of `main-panel`) until after `app-db` is right. We don't want any rendering (of `main-panel`) until after `app-db` is right.
Okay, so that's enough of teasing-out the issues. Let's see a quick sketch of the entire pattern. It is very straight-forward: Okay, so that's enough of teasing-out the issues. Let's see a
quick sketch of the entire pattern. It is very straight-forward:
## The Pattern ## The Pattern
@ -104,12 +122,12 @@ Okay, so that's enough of teasing-out the issues. Let's see a quick sketch of th
(re-frame/reg-sub ;; the means by which main-panel gets data (re-frame/reg-sub ;; the means by which main-panel gets data
:name ;; usage (subscribe [:name]) :name ;; usage (subscribe [:name])
(fn [db _] (fn [db _]
(:name db))) (:display-name db)))
(re-frame/reg-sub ;; we can check if there is data (re-frame/reg-sub ;; we can check if there is data
:initialised? ;; usage (subscribe [:initialised?]) :initialised? ;; usage (subscribe [:initialised?])
(fn [db _] (fn [db _]
(not (empty? @db)))) ;; do we have data (not (empty? db)))) ;; do we have data
(defn main-panel ;; the top level of our app (defn main-panel ;; the top level of our app
[] []
@ -136,10 +154,10 @@ Okay, so that's enough of teasing-out the issues. Let's see a quick sketch of th
This pattern scales up easily. This pattern scales up easily.
For example, imagine a more complicated scenario in which your app is not fully initialised until 2 backend services supply data. For example, imagine a more complicated scenario in which your app
is not fully initialised until 2 backend services supply data.
Your `main` might look like this: Your `main` might look like this:
```Clojure ```Clojure
(defn ^:export main ;; call this to bootstrap your app (defn ^:export main ;; call this to bootstrap your app
[] []
@ -156,17 +174,34 @@ Your `:initialised?` test then becomes more like this sketch:
(reg-sub (reg-sub
:initialised? ;; usage (subscribe [:initialised?]) :initialised? ;; usage (subscribe [:initialised?])
(fn [db _] (fn [db _]
(and (not (empty? @db)) (and (not (empty? db))
(:service1-answered? @db) (:service1-answered? db)
(:service2-answered? @db))))) (:service2-answered? db)))))
``` ```
This assumes boolean flags are set in `app-db` when data was loaded from these services. This assumes boolean flags are set in `app-db` when data was loaded from these services.
## A Cheat - Synchronous Dispatch ## A Cheat - Synchronous Dispatch
In simple cases, you can simplify matters by using `(dispatch-sync [:initialise-db])` in the main entry point function. The [Simple Example](https://github.com/Day8/re-frame/blob/8cf42f57f50f3ee41e74de1754fdb75f80b31775/examples/simple/src/simpleexample/core.cljs#L110) and [TodoMVC](https://github.com/Day8/re-frame/blob/8cf42f57f50f3ee41e74de1754fdb75f80b31775/examples/todomvc/src/todomvc/core.cljs#L35) example both use `dispatch-sync` to initialise the app-db. This causes the event to jump to the front of the line and causes it to execute immediately, which is fine for the initial data load in a simple app but can lead to problems elsewhere. As your app gets more complicated, it is strongly suggested that you use the regular `dispatch` function where possible. If you are using `dispatch-sync` and run into weird errors, there's a pretty high chance that it's the culprit. In simple cases, you can simplify matters by using `(dispatch-sync [:initialise-db])` in
the main entry point function. The
[Simple Example](https://github.com/Day8/re-frame/blob/8cf42f57f50f3ee41e74de1754fdb75f80b31775/examples/simple/src/simpleexample/core.cljs#L110)
and [TodoMVC Example](https://github.com/Day8/re-frame/blob/8cf42f57f50f3ee41e74de1754fdb75f80b31775/examples/todomvc/src/todomvc/core.cljs#L35)
both use `dispatch-sync` to initialise the app-db.
## Services `dispatch-sync` acts like a function call
and ensures the event is handled immediately, which is useful for initial data load in a
simple app - you don't need to guard against uninitialised data - you don't need
a `top-panel` (introduced above).
Remember when we used `dispatch` to request the data in our `main` function? What would those event handlers looks like? Let's go to [Talking to Servers](Talking-To-Servers.md) and find out! But don't get into the habit of using `dispatch-sync`. It is the right
tool in this context and, sometimes, when writing tests, but
`dispatch` is the staple to be used everywhere else.
## Loading Initial Data From Services
Above, in our example `main`, we used `dispatch` to request data
from a backend service. What would the event handlers look like
which initiate the GETs?
Let's go to [Talking to Servers](Talking-To-Servers.md) and find out!