Skip to content

Bootstrap An Application

Mike Thompson edited this page Aug 30, 2016 · 32 revisions

This document is out of date.
A more modern version can be found within the repo

--

To bootstrap a re-frame application, you need to:

  1. register handlers (subscription and event handlers)
  2. kickstart reagent (views)
  3. get the right data into app-db which might be a merge of:
  • Some default values
  • Values stored in LocalStorage
  • Values obtained via service calls to server
  • etc, etc

Point 3 is the interesting bit and will be the main focus of this page, but let's work our way through them ...

Register Handlers

Generally, there's nothing to do because this happens automatically at (js) script load time, because you declared and registered your handlers like this:

(register-handler           ;; handler will be registered automatically
    :some-id
    (fn [db v]  
         .. do some state change based on db and v)

Kick Start Reagent

Create a function main which does a reagent/render of your root reagent component main-panel:

(defn main-panel       ;; my top level reagent component
  []
  [:div "Hello DDATWD"])

(defn ^:export main     ;; call this to bootstrap your app
  []
  (reagent/render [main-panel]
                  (js/document.getElementById "app")))

The Right Data

Let's rewrite our main-panel component to use a subscription:

(register-sub        ;; a new subscription handler
   :name             ;; usage (subscribe [:name])
   (fn  [db]
     (reaction (:name @db)))   ;; pulls out :name
     
(defn main-panel 
  []
  (let [name  (subscribe [:name])]  ;; <--- a subscription  <---
    (fn []
      [:div "Hello " @name]))))   ;; <--- use the result of the subscription

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.

That will require:

  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.

Note: app-db initially contains {}

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.

Here's an event handler for that purpose:

(register-handler
   :initialise-db             ;; usage: (dispatch [:initialise-db])
   (fn 
     [_ _]                   ;; Ignore both params (db and v). 
     {:display-name "DDATWD" ;; return a new value for app-db
      :items [1 2 3 4]}))

We'll need a dispatch :initialise-db to get that event handler executed. main seems like the natural place:

(defn ^:export main
  []
  (dispatch [:initialise-db])   ;;  <--- this is new 
  (reagent/render [main-panel]
                  (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 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.

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

(register-sub       ;; the means by which main-panel gets data
  :name             ;; usage (subscribe [:name])
  (fn  [db]
    (reaction (:name @db))))

(register-sub       ;; we can check if there is data
  :initialised?     ;; usage (subscribe [:initialised?])
  (fn  [db]
    (reaction (not (empty? @db))))   ;; do we have data

(defn main-panel    ;; the top level of our app 
  []
  (let [name  (subscribe :name)]   ;; we need there to be good data
    (fn []
      [:div "Hello " @name]))))

(defn top-panel    ;; this is new
  []
  (let [ready?  (subscribe [:initialised?])]
    (fn []
      (if-not @ready?         ;; do we have good data?
        [:div "Initialising ..."]   ;; tell them we are working on it
        [main-panel]))))      ;; all good, render this component

(defn ^:export main     ;; call this to bootstrap your app
  []
  (dispatch [:initialise-db])
  (reagent/render [top-panel]
                  (js/document.getElementById "app")))

Scales Up

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.

Your main might look like this:

(defn ^:export main     ;; call this to bootstrap your app
  []
  (dispatch [:initialise-db])           ;; basics
  (dispatch [:load-from-service-1])     ;; ask for data from service-1
  (dispatch [:load-from-service-2])     ;; ask for data from service-2
  (reagent/render [top-panel]
                  (js/document.getElementById "app")))

Your :initialised? test then becomes more like this sketch:

(register-sub
  :initialised?          ;; usage (subscribe [:initialised?])
  (fn  [db]
    (reaction (and  (not (empty? @db))
                    (:service1-answered? @db)
                    (:service2-answered? @db)))))

This assumes boolean flags are set in app-db when data was loaded from these services.

Services

Remember those dispatch we did in main to request the data. What would the handlers looks like?

They would have to kick off an HTTP GET to get that json from the server, right?

(register-handler
  :load-from-service-1
  (fn
    [db _]
    (ajax.core/GET
      "/some/url"
      {:handler       #(dispatch [:process-service-1-response %1])
       :error-handler #(dispatch [:bad-response %1])})
     db))    ;; pure handlers must return a db (unchanged in this case)

Now we write the handlers for GET success and failure:

(register-handler            ;; when the GET succeeds 
  :process-service-1-response    
  (fn
    [db [_ response]]
    (-> db
        (assoc :service1-answered? true)      ;; set flag saying we got it
        (assoc :service1-data (js->clj response))))  ;; fairly lame processing

The :bad-response failure handler is left as an exercise for the reader.

A Cheat

In simple cases, you can simplify matters by using (dispatch-sync [:initialize-db]) in the main entry point function. The TodoMVC example contains this cheat. (The cheat is using dispatch-sync instead of dispatch).