re-frame/docs/Talking-To-Servers.md

5.7 KiB

Talking To Servers

This page describes how a re-frame app might "talk" to a backend HTTP server.

We'll assume there's a json-returning server endpoint at "http://json.my-endpoint.com/blah". We want to GET from that endpoint and put a processed version of the returned json into app-db.

Triggering The Request

The user often does something to trigger the process.

Here's a button which the user could click:

(defn request-it-button
  []
  [:div {:class "button-class"
         :on-click  #(dispatch [:request-it])}  ;; get data from the server !!
         "I want it, now!"])

Notice the on-click handler - it dispatches the event [:request-it].

The Event Handler

That :request-it event will need to be "handled", which means an event handler must be registered for it.

We want this handler to:

  1. Initiate the HTTP GET
  2. Update a flag in app-db which will trigger a modal "Loading ..." message for the user to see

We're going to create two versions of this event handler. First, we'll create a problematic version of the event handler and then, realising our sins, we'll write a second version which is a soaring paragon of virtue. Both versions will teach us something.

Version 1

We're going to use the cljs-ajax library as the HTTP workhorse.

Here's the event handler:

(ns my.app.events                   ;; <1>
   (:require [ajax.core :refer [GET]]
             [re-frame.core :refer [reg-event-db]))

(reg-event-db        ;; <-- register an event handler
  :request-it        ;; <-- the event id
  (fn                ;; <-- the handler function
    [db _]
   
    ;; kick off the GET, making sure to supply a callback for success and failure
    (GET
      "http://json.my-endpoint.com/blah"
      {:handler       #(dispatch [:process-response %1])   ;; <2> further dispatch !!
       :error-handler #(dispatch [:bad-response %1])})     ;; <2> further dispatch !!
      
     ;; update a flag in `app-db` ... presumably to cause a "Loading..." UI 
     (assoc db :loading? true)))    ;; <3> return an updated db 

Further Notes:

  1. Event handlers are normally put into an events.cljs namespace
  2. Notice that the GET callbacks issue a further dispatch. Such callbacks should never attempt to close over db themselves, or make any changes to it because, by the time these callbacks happen, the value in app-db may have changed. Whereas, if they dispatch, then the event handlers looking after the event they dispatch will be given the latest copy of the db.
  3. event handlers registered using reg-event-db must return a new value for app-db. In our case, we set a flag which will presumably cause a "Loading ..." UI to show.

Successful GET

As we noted above, the on-success handler itself is just (dispatch [:process-response RESPONSE]). So we'll need to register a handler for this event too.

Like this:

(reg-event-db                   
  :process-response             
  (fn
    [db [_ response]]           ;; destructure the response from the event vector
    (-> db
        (assoc :loading? false) ;; take away that "Loading ..." UI 
        (assoc :data (js->clj response))))  ;; fairly lame processing

A normal handler would have more complex processing of the response. But we're just sketching here, so we've left it easy.

There'd also need to be a handler for the :bad-response event too. Left as an exercise.

Problems In Paradise?

This approach will work, and it is useful to take time to understand why it would work, but it has a problem: the event handler isn't pure.

That GET is a side effect, and side effecting functions are like a well salted paper cut. We try hard to avoid them.

Version 2

The better solution is, of course, to use an effectful handler. This is explained in detail in the previous tutorials: Effectful Handlers and Effects.

In the 2nd version, we use the alternative registration function, reg-event-fx , and we'll use an "Effect Handler" supplied by this library https://github.com/Day8/re-frame-http-fx. You may soon feel confident enough to write your own.

Here's our rewrite:

(ns my.app.events                  
   (:require
      [ajax.core :as ajax]        
      [day8.re-frame.http-fx]  
      [re-frame.core :refer [reg-event-fx]))

(reg-event-fx        ;; <-- note the `-fx` extension
  :request-it        ;; <-- the event id
  (fn                ;; <-- the handler function
    [{db :db} _]     ;; <-- 1st argument is coeffect, from which we extract db 
   
    ;; we return a map of (side) effects
    {:http-xhrio {:method          :get
                  :uri             "http://json.my-endpoint.com/blah"
                  :format          (ajax/json-request-format)
                  :response-format (ajax/json-response-format {:keywords? true}) 
                  :on-success      [:process-response]
                  :on-failure      [:bad-response]}
     :db  (assoc db :loading? true)}))

Notes:

  1. Our event handler "describes" side effects, it does not "do" side effects
  2. The event handler we wrote for :process-response stays as it was

Previous: Loading Initial Data       Up: Index       Next: Subscribing to External Data