Skip to content

Effectful Event Handlers

Mike Thompson edited this page Jul 24, 2016 · 25 revisions

This page describes features in the v0.8.0 release of re-frame. Comment and discussion on this feature is happening here. Please feel free to correct any typos and mistakes. Thanks.

This tutorial shows you how to implement pure event handlers that side-effect. Yes, a surprising claim.

In the process, via the explanations provided, you'll also get insight into how you might use an alternative to app-db, like perhaps a DataScript database.

Event Handlers

Not all event handlers are pure. The great majority are but there'll always be some handlers which need to side-effect.

First, the good...

Here we register a pure event handler:

(reg-event
   :my-event
   (fn [db [_ a]]    ;; <--- lovely and pure
       (assoc db :flag true)))

That fn handler is passed values, returns a value. No side-effects.

Now, to the more troublesome...

This handler is not pure:

(reg-event
   :my-event
   (fn [db [_ a]]
       (dispatch [:do-something-else 3])    ;; oops, side-effect
       (assoc db :flag true)))

That handler fn is passed values, returns a value, but ... it has a side-effect. That dispatch queues up another event to be processed. It changes the world.

And, this is not pure either:

(reg-event
   :my-event
   (fn [db [_ a]]
       (GET "http://json.my-endpoint.com/blah"   ;; dirty great big side-effect
            {:handler       #(dispatch [:process-response %1])
             :error-handler #(dispatch [:bad-response %1])})  
       (assoc db :flag true)))

Sure, this approach works. But that dirty great big side-effect doesn't come for free.

So why Is This bad, Again?

Both of those impure event handlers will work. So what's the problem?

A re-frame application proceeds step by step, like a reduce. From the README:

at any one 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 that time. The combining function for this reduce is the set of registered event handlers.

Such a collection of events is replay-able which is a dream for debugging and testing. But only when all the handlers are pure. Handlers with side-effects (like that HTTP GET, or the dispatch) pollute the replay process, inserting extra events, etc, which ruins the process.

And, it is hard to test these impure handlers. Did the handler GET the right URL? Did it dispatch the right event? Now we have to start mocking things out. Everything gets harder.

So, impure handlers cause a small collection of irritating paper cuts. It isn't a disaster, but we'd like to do better.

Why All The Side Effects?

Side-effecting event handlers are inevitable.

Event handlers implement the control logic of your re-frame app, and that means dealing with the mutative world of servers, databases, windows.location, cookies, etc.

There's just no getting around that. So it sounds like we're stuck. No solution?

Well, luckily a small twist in the tale makes a profound difference. Instead of creating event handlers which do side-effects, we'll instead get them to cause side-effects.

Doing vs Causing

Above, I claimed that this fn event handler was pure:

(reg-event
   :my-event
   (fn [db _]
       (assoc db :flag true)))

Takes a db value, returns a db value. No side-effects. Pure!

All true, but ... this purity is only possible because re-frame is doing the necessary side-effecting.

Wait on. What "necessary side-effecting"?

Well, app-db is a ratom, right? It contains the application state and, after each event handler runs, it must be reset! to the newly returned value. re-frame's steps for each event are:

  1. extract the value (a map) from app-db (a ratom)
  2. call the registered event handler with this db value as the first argument
  3. reset! the returned value back into app-db

So, we get to live in our ascetic functional world because re-frame is looking after the "necessary side-effects" on app-db.

Et tu, React?

Turns out it's the same pattern with Reagent/React.

We get to write a nice pure component:

(defn say-hi
  [name]
  [:div "Hello " name])

and Reagent/React mutates the DOM for us. The framework is looking after the "necessary side-effects".

Pattern Structure

Pause and look back at say-hi. I'd like you to view it through the following lens: it is a pure function which returns a description of the side-effects required. It says: add a div element to the DOM.

Notice that the description is declarative. We don't tell React how to do it.

Notice also that it is data. Hiccup is just vectors and maps.

This is a big, important concept. While we can't get away from certain side-effects, we can program using pure functions which describe side-effects, declaratively, in data and let the backing framework look after the "doing" of them. Efficiently. Discreetly.

Let's use this pattern to solve the side-effecting handler problem.

The Two Part Plan

From here, two steps:

  1. Work out how event handlers can declaratively describe side-effects, in data.
  2. Work out how re-frame can do the "necessary side-effecting". Efficiently and discreetly.

Part 1 Of Plan

So, how would it look if event handlers returned side-effects, declaratively, in data?

Here is an impure handler:

(reg-event
   :my-event
   (fn [db [_ a]]
       (dispatch [:do-something-else 3])    ;; Eeek, side-effect
       (assoc db :flag true)))

Here it is re-written so as to be pure:

(reg-event
   :my-event
   (fn [db [_ a]]
      {:db  (assoc db :flag true)          ;; side-effect we want on db
       :dispatch [:do-something-else 3]})) ;; side-effect from dispatching

The handler is returning a data structure which describes two side-effects:

  1. update app-db (application state) with this new db value
  2. dispatch an event

Above, the impure handler did a dispatch side-effect, while the pure handler described a dispatch side-effect.

Another Example

The impure way:

(reg-event
   :my-event
   (fn [db [_ a]]
       (GET "http://json.my-endpoint.com/blah"   ;; dirty great big side-effect
            {:handler       #(dispatch [:process-response %1])
             :error-handler #(dispatch [:bad-response %1])})  
       (assoc db :flag true)))

the pure, descriptive way:

(reg-event
   :my-event
   (fn [db [_ a]]
       {:http {:method :get
               :url    "http://json.my-endpoint.com/blah"
               :on-success  [:process-blah-response]
               :on-fail     [:failed-blah]}
        :db   (assoc db :flag true)}))

Again, the old way did a side-effect (Booo!) and the new way describes, declaratively, in data, the side-effects required (Yaaa!).

Effects and Coeffects

Time for the next step.

So far we've been experimenting with side-effects but that's only one half of the picture. There are actually two concepts at play here:

  • Effects - what your event handler does to the world (aka side-effects)
  • Coeffects - what your event handler requires from the world (aka side-causes)

So now we'll talk about the 2nd part: coeffects

Re-imagining db

Above, I proposed this event handler:

(reg-event
   :my-event
   (fn [db [_ a]]
      {:db  (assoc db :flag true)          ;; side-effect we want on db
       :dispatch [:do-something-else 3]})) ;; side-effect from dispatching

I'd now like to change the name of the first handler argument from db to world, and pass in this structure {:db db}. Ie. the argument is now a map, and the key :db has the value in app-db.

So, we rewrite our handler like this:

(reg-event
   :my-event
   (fn [world [_ a]]    ;; world, not db as parameter 
     {:db       (assoc (:db world) :flag  true)         
      :dispatch [:do-something-else 3]}))

or perhaps like this (shrug):

(reg-event
   :my-event
   (fn [world [_ a]]      ;; world, not db as parameter 
     (-> world            ;; world is {:db db}
       (assoc-in [:db :flag]  true)         
       (assoc-in [:dispatch] [:do-something-else 3]))))

So the 1st parameter to the event handler, world, is the "state of the world" or the "computational context" in which the handler is to run. It is the coeffect.

Alternative Worlds

This rewrite means that world can now be more than db. It can include whatever other "computational context" the handler needs to perform its computation. Most of the time, that's just what's in app-db, but "other aspects of the computation context" are possible.

For example, what if a handler needed to know the current datetime? Or, it needed a random number? Or, what if some state was stored in a DataScript database?

All possible, but you must know how "new worlds" are created ...

New Worlds

A "new world" is created each and every time an event handler is run. A brand spanking new world. And it is the event handler's middleware stack which makes that happen.

This can be a surprise to even experienced re-frame users. After all, look at this event handler registration ... there's no apparent middleware:

(reg-event
   :my-event         ;; no apparent middleware !!!!!
   (fn [db _]
       (assoc db :flag true)))

But, don't be fooled. re-frame.core/reg-event exists for only one reason: to install the pure middleware on all your handlers in the right place within the stack. Here's proof

It is pure middleware which does the important work. First, it takes the value out of app-db, makes it "the world" by providing it to the event handler as the first argument and then, after the handler returns, it is again pure which uses reset! to mutate app-db.

So, pure is that bit of re-frame which does BOTH effects and coeffects. It obtains and provides db to an event handler and, afterwards, it does the "necessary side-effecting" to put data back into app-db.

Aside

While pure is central, re-frame's other middleware can also contribute to
effects and coeffects. For example, undoable, snapshots the value in app-db. And after is used in the todomvc example to write out to LocalStore.

So, within re-frame, effects and coeffects are done with middleware. We're now going to up the ante a bit.

Plan Review

So, how are we going?

Earlier, I presented a two step plan:

  1. Work out how event handlers can declaratively describe side-effects, in data.
  2. Work out how re-frame can action these side-effecting descriptions, efficiently and discreetly

We have a solution for 1.

And, regarding point 2, we now know that middleware is the answer, somehow.

Let's give this new middleware a name: fx - (effects, geddit?)

Below I provide a specific solution for fx but before we get there:

  1. remember, you can write your own middleware. It isn't hard.
  2. fx is definitely not the only way to do this.

Meet fx

fx will be an alternative for pure.

You will continue to use pure for regular handlers whose effects and coeffects are limited to app-db. No change there.

But you will use fx for event handlers which need additional effects and coeffects.

pure is largely invisible because it gets "installed" by re-frame.core/reg-event. So too it will be for fx.

A new registration function, re-frame.core/reg-event-fx (notice that -fx on the end), will invisibly add fx to the middleware stack of the registered handler.

Aside: remember that each event handler has its own middleware stack. So, some can have a pure stack, and some fx stacks.

Use will look like this:

(reg-event-fx      ;; <-- use alternative registration (-fx on the end)
   :my-event
   (fn [world [_ a]]    ;; world, instead of db     
     {:db       (assoc (:db world) :flag  a)         
      :dispatch [:do-something-else 3]}))   ;; return effects

Aside: Instead of fx I almost named it io, which is a nod towards Haskell. The language. Not James Haskell, the current English Rugby openside breakaway, who I often claim is functional but lazy. My Rugby friends have no idea what I'm talking about - "Mike, you're being too harsh. He's in career-best form". Honestly, its pearls before swine.

Implementing fx

fx will need to:

  1. provide a world of {:db db} . This is what the rest of the middleware stack will see, including the handler being registered
  2. action the side effects described in the data returned by the handler

Here is an initial sketch of fx in code:

(defn fx
  [handler]
  (fn io-handler
    [app-db event-vec]                   
    (let [world   {:db @app-db}           
          result (handler world event-vec)]   ;; call the event handler with world
       ; HERE IS WHERE WE ACTION THE EFFECTS in result
       (if-let [db (:db result)]
          (reset! app-db db))))

If writing middleware is a mystery to you, do not be alarmed. Just know that we must now solve a problem: how should fx action the side effects returned by an event handler.

fx may get a handler return value like:

{:db  X
 :http  Y
 :dispatch Z}

Actioning the :db part would be straightforward. But what about :http and :dispatch? And what to do if result contains the key :abc? The set of possible effects is open ended.

Solutions

re-frame provides a plugable way of registering handlers for effects: reg-fx. You nominate a handler which will action the side effects described by a key in an effects map.

Example use:

(re-frame.core/reg-fx
   :http                 ;; if the key :http is found in an effect map, then ...
   (fn [val]             ;; ... action it via this handler function. "val" is the value of the key.
       ....))

So, to process an effects map, fx finds all the keys in the map (:http :dispatch :db ?), looks up the associated handler for each, and calls these handlers, passing in the value of that key as the one argument.

Just to be clear, if the returned effects looked like:

{:db  {}
 :dispatch  [:they-said-yes]
 :http {:method :get
        :url    "http://google.com/secret-dark-web/naughty-bits"}}

Then fx would handle :http by calling the registered handler with one argument:

{:method :get
 :url    "http://google.com/secret-dark-web/naughty-bits"}

Order Of Effects

There isn't an ordering.

At this point, fx does not allow you to control the order in which side effects occur. It is arbitrary. If you feel you need ordering - really need it - then please open an issue and explain the usecase. Ordering isn't in fx currently, because we don't have a usecase.

If it were to be needed, it would be handled by allowing handler to return a vector of maps. The effects in each map would be handled in the order supplied.

Like this:

(fn event-handler 
  [world event-v]
  [{:db  (assoc (:db world) :flag true)}
   {:http  {:method :get  :url "http://dark.google.com/" }])   

Plug-ability

You don't always want effects to be actioned. Sometimes, if you are testing or debugging, you want effects to become noops.

Here's what a noop effects handler would look like:

(defn fx-noop [val])

Here's how :http would be turned into a noop

(re-frame.core/reg-fx  :http  noop)    ;; handler now does nothing 

XXX make reg-fx return the previous handler, so it can be reinstated? XXX or should this be done in a with-redefs kinda way?

Standard Effects

XXX list of standard effects.


Ramblings follow. Don't read.

Some candidate effects:

  • dispatch
  • flush-dom (force dom writes)
  • undo
  • localstore writes
  • database queries (rethinkdb) ?
  • http get or post
  • cookies
  • event sniffing

XXX how would you do multiple GET ? Multiple dispatches? Give a vector I guess
XXX should they be given the entire returned value, or just their part of the try?