Skip to content

Alternative dispatch, routing & handling

Mike Thompson edited this page Aug 19, 2016 · 25 revisions

This document is awaiting a rewrite - it is out of date.
The approach explained below will still work, but version 0.8.0 introduced new features/techniques meaning there's now a more modern way to do things.
Once rewritten, it will be moved within the repo

--

re-frame's reference implementation makes choices in the area of event dispatching/routing and handling.

Just to be clear: the re-frame pattern doesn't make these choices (it can be implemented in a variety of ways), but the reference implementation does.

Choices like:

  1. dispatch sends a vector (not a map)
  2. you can only register one handler per event
  3. you can't unregister handlers

Not For Everyone

But these choices won't suit everyone.

In some cases, you might want:

  • to dispatch maps, not vectors
  • to have certain handlers see all events.
  • to register multiple handlers for the one event.
  • to register and unregister event handlers

On the surface, it seems impossible for the reference implementation to handle any of this.

Not so fast. The reference implementation is a more flexible base than you might realise initially. You can build on top of it and, if it really can't do what you want, you can insert your own routing method.

In fact, I can imagine a future in which a few different approaches are implemented as pluggable layers on top of the reference implementation.

Which is not to say you won't just fork and do your own thing. All good. But, before you do, be aware that there is more flexibility in re-frame's base implementation than you might imagine.

Here's some examples ...

Dispatching Maps

You want to dispatch maps, not vectors.

In dispatched maps, a key :event-id identifies the handler. (Not the first element of the vector as happens in the reference implementation).

So, instead of doing this:

;; event is identified by 1st element of vector
(dispatch [:something [ 1 2 3 4]])    

you'd prefer to do this:

;; dispatch a map, with key :event-id which identifies the event
(dispatch {:event-id :something :values [ 1 2 3 4]}) 

Here's a sketch for how to do it using base reframe ...

First, write an alternative dispatch:

(defn my-dispatch
   [m]
   (dispatch [(:event-id m) m]))    ;; uses re-frame's dispatch

Make all your components use it (instead of the standard dispatch):

(my-dispatch  {:event-id  :something :values [ 1 2 3 4]})  ;; dispatch a map!

Wait on, that's clearly cheating you say. To which I'd shrug in a charming mediterranean way and move on to showing you the implementation of the handler:

(register-handler
   :something
   (fn 
      [db [_ m]]
      ... use the map m in here))

That handler gets easy access to the map m. Works for me.

A "See All" Handler

Whenever an event is dispatched I want two handlers to be called:

  1. a special "I see everything" handler
  2. the handler which is specific to the event

Turns out we can do that.

Again, let's use a variation on dispatch:

(defn my-dispatch
  [event-v]
  ;; dispatch but first prepend an event-id
  (dispatch (cons :must-see event-v)))   

Use it like this

(my-dispatch  [:something 10 20])

which will actually end up as:

(dispatch [:must-see :something 10 20])   ;; :must-see is on the front

And then:

(register-handler 
   :must-see         ;; This handler sees every event!! 
   (fn [db v]
      ;; dispatch to the 2nd handler, by getting rid of the leading :must-see
      ;; is async ... next handler called after this handler returns
      (dispatch (vec (rest v)))    ;; using rest - get rid of :must-see

      ;; now do somestuff
      db)     ;; always return the new state of the database

I know, I know. Outrageous, right? Want to play some cards? For money?

BTW, you might also be able to do this by putting a piece of middleware on every handler, and having it do the necessary processing before the real handler gets to work. Middleware is flexible stuff.

Multiple handlers For The One Event

Imagine that you are writing a game.

To drive the animations, you want to (dispatch [:next-tick]) at a regular cadence (16ms?) and, in response, have N different event handlers run.

Except, re-frame doesn't allow for that. Only one handler can be registered for an event like :next-tick.

And there's a further problem: in our game, the set of N different handlers is dynamic. Over time, handlers are being added and removed. And re-frame doesn't allow for de-registration.

Here's a solution sketch ...

Store the event-ids for all N handlers in app-db itself in a set of :tick-handler-ids.

Provide a way for tick handler ids to be added:

;; usage: (dispatch [:add-tick-handler-ids [:some-id :another-id ...]])
(register-handler
 :add-tick-handler-ids trim-v
 (fn [db [ids]] (update db :tick-handler-ids (fnil into #{}) ids)))

and removed:

;; usage: (dispatch [:remove-tick-handler-ids [:id1 :id2 ...]])
(register-handler
 :remove-tick-handler-ids trim-v
 (fn [db [ids]] (update db :tick-handler-ids #(apply disj % ids))))

And finally, this is what happens each tick:

(register-handler
 :next-tick
 (fn [db v]
   (re-trigger-timer)                      ;; run again in 16 ms, see below
   (transduce                              ;; run all the handlers, updating db
    (map re-frame.handlers/lookup-handler)
    (completing (fn [db h] (h db v)))
    db (:tick-handler-ids db))))

Use reagent's next-tick capability (based on requestAnimationFrame) to dispatch (at 16ms in the future):

(defn re-trigger-timer 
  [] (reagent/next-tick (fn [] (dispatch [:next-tick])))) 

Since register-handler automatically injects the pure middleware, any of the handlers registered via the :add-tick-handler-ids event must be registered via the more lower level re-frame.handlers/register-base fn instead, like so:

(re-frame.handlers/register-base
  :tick-child1
  (fn [db _] (info :tick-child1) db))

In this case, the db value passed to such a handler is already "pure", since it's passed directly via the pure :next-tick handler above. However, this too means that you cannot dispatch normal events to such handlers anymore. This should not be an issue in practice, since they're continuously triggered by :next-tick...

The only other offensive public act here was the use of re-frame.handlers/lookup-handler within the :next-tick handler. I won't tell if you don't. But if it worries you too much, then you'd go to a bit more trouble and write your own mini registration and lookup infrastructure specifically for these tick handlers.

Some Crazy Hard Requirement

What happens if the routing requirement is just too different? For example ...

Instead of registering handlers for a specific event-id, I'd like to register handlers for a "regex-like pattern". And if a dispatched event is matched by any handler's registered pattern, then that handler should be invoked. And also handlers have a priority, to control ordering ... etc, etc.

There's no way to do that in base re-frame. The registration would be different, the routing is different. You are going to have to write them.

But we can kinda plug this alternative router and registration into the base re-frame approach, adding a layer on top.

Sketch:

  1. register a single handler with re-frame. Just one.

  2. Create a dispatcher which sends all events to that one handler.

      (defn my-dispatch
       [event-v]
       (dispatch (cons :there-can-be-only-one event-v)))   ;; prepend an event-id

Use this dispatcher in all components. No hint of the fact they all go to one place.

  1. In this single handler for :there-can-be-only-one, you can do whatever you want in the way of further routing based on whatever alternative registration schemes you also implemented.

If you go this way, you'll be using very little of re-frame. The dispatch might looks similar, but the routing and registering is done in whatever advanced/complicated way you need. But you do it by plugging into re-frame, and building on top of it, rather than replacing it outright.

In the future, we might try to figure out how to do a "real" plugins API. The above is as good as we've got for the moment.