Description
re-frame will soon allow:
events
to bemaps
.queries
to bemaps
.
Events
Assuming this ns
:
(ns my-app.my-namespace
(:require [re-frame.core :as rf]))
You can still dispatch events which are vectors, as before:
(rf/dispatch [:some-event-id arg1])
but, now, you can also dispatch a map:
(rf/dispatch {::rf/eid :some-event-id :some-thing arg1])
Within the map being dispatched, notice the key :re-frame.core/eid
via the aliased namespace. eid
means event-id
. The value of that key is the event identifier. The other keys in the map are application-specific.
Queries
You can still subscribe
using a vector, as before:
(rf/subscribe [:some-query-id arg1])
but, now, you can also subscribe using a map:
(rf/subscribe {::rf/qid :some-query-id :a-name arg1])
Within the map supplied to subscribe
, notice the key :re-frame.core/qid
via the aliased namespace. The value of that key is the query-id. The other keys in the map are application-specific.
Writing Handlers
You write handlers for map-encoded events as you would other handlers, it is just that the event
argument is a map not a vector:
(rf/reg-event-fx
:token
(fn [coeffects event]
... event is a map))
Open Questions
- is
:re-frame.core/eid
a good choice for map key? Other? - is
:re-frame.core/qid
a good choice for map key? Other?
They should be namespaced, yes? Is eid
too cryptic? Prefer a longer more explicit name?
I'm unhappy with ::rf/eid
. That's a horrible mouthful for a newbie. Maybe the tutorials just teach the vectors way and avoid exposing newbies to event maps early on.
Timing
@superstructor will check-in a first cut (to master
) within a day or so. But a release is probably a week away, We'll wait for feedback. And bring a couple of libraries and utilities along.
Likely Objections
Question: Won't this approach make dispatching and subscribing more verbose?
Answer: Yes, it will. If you find that offensive, you don't have to use it. You can keep doing exactly what you are doing now. It is just that being able to use maps more easily solves some problems and we wanted to unlock that flexibility.
Question: Why not just use this partial approach (dispatch [:event-id {:param1 arg1}])
. Ie. make events a 2-vector of [event id a-map]
. That would be backward compatible.
Answer: We did consider it. But we felt that design would be lukewarm coffee - we felt it was right to go all-in on maps.
A Breaking Change?
We are "growing the API" by allowing events to be maps. So, in theory, this is not a breaking change.
However, that's cold comfort. If you do start to use these two new features in your application, you might find that
certain re-frame libraries and utilities might break because they expect vectors, and you are giving maps.
These libraries will have to be changed to accommodate the expanded API flexibility. So don't use maps in your application until all dependent libraries and utilities are updated.
We are aware that the following utilities/libraries will need to be changed:
- re-frisk
- re-frame-10x
- re-frame-http-fx
- re-frame-async-flow
Are we forgetting any?
Why?
Terse Version
For software systems, the arrow of time and the arrow of entropy are aligned. Changes inevitably happen.
When we use vectors in both these situations we are being "artificially placeful". The elements of the vector have names, but we are pretending they have indexes. That little white lie has some benefits (is terse and minimal) but it also makes the use of these structures a bit fragile WRT to change. Names (truth!) are more flexible and robust than indexes (a white lie).
The benefit of using names accumulate on two time scales:
- within a single execution of the app (as data moves around on the inside of your app)
- across the maintenance cycle of the app's codebase (as your app's code changes in response to user feedback)
Longer
Back in 2014, I wrestled with how to model events in re-frame. Should I choose to represent events with vectors or maps?
In the end, I choose vectors. If I remember correctly, that was partly because I saw Pedestal App
doing messaging with vectors and it was designed by VIPs (very important programmers). And partly because
I believe aesthetics/ergonomics matter a lot and, at the dispatch point, using a vector is minimal and neat.
I was aware I was giving up something flexibility-wise, but it didn't feel like too much. Tradeoffs.
Mike, did you just try to shift blame to the Pedestal App team?
Oh. It was that obvious, was it? Look, I was a young, foolish and impressionable functional programmer led astray.
Now, in the intervening years, I got wiser slowly. I have become aware that some "falsely positional"
arrangements - arguments to functions, for example - are inherently more fragile WRT to "evolution" (aka the passage of time).
But I thought I had got away with it. I thought that I had managed to get all the benefits of more minimal aesthetics,
without the downside of fragility. Until last week.
I was in a design session with a colleague, Denis, and we were kicking about ideas when I was suddenly struck a bit speechless because I realized that the placefulness of vector events had completely stopped me from considering various design options. If I allowed myself to think about the problem at hand with events being maps, the design fell out kinda easily because that better allowed for evolution (of data over time, within a running app - which was one of the two time scales I identified above).
I hadn't got away with it after all, and this issue, which became all too easy via maps, has been an irritating
pebble in my shoe for waaaaay too long. I'd had blinkers on. And I'd been trying to work around the fake placefulness of using vectors for this purpose.
So, here we are. I'm out of that cage. Change is afoot.
Final thought: vectors really have served us pretty well. There'a only a couple of places where the additional flexibility of maps is going to be useful, but they are important ones.
Weak Example
The following example is a little weak, but I want to keep things simple. Sometimes, you need to
do some very CPU intensive work. Something which will tie up the single browser thread available to you.
You need to update the DOM (show the twirly?) and then hog that thread doing that CPU intensive work.
So, this is a two step process. Update app-db
to show the twirly, and then wait for the next animation frame
to have passed so the UI is drawn. And only then start hogging the CPU. If you hog the CPU before the animation
frame, the twirly never gets shown.
If you have :flush-dom
metadata on an event you dispatch, re-frame will wait until after the next
animation frame before handling it.
Okay, so that's the setup. We're going to need a two step process.
But of course, this is all implementation detail. Somewhere within an app, a button will dispatch
an event which says "do calc" but that view knows nothing about the two steps.
Let's model the event emitted by the button click as a map; (dispatch {::rf/eid :do-calc})
The handler looks like this:
(rf/reg-event-fx
:do-calc
(fn [{:keys [db]} {:keys [flushed?] :or {flushed? false} :as event}]
(if-not flushed?
;; step 1 -
{:db (assoc db :twirly true)
:dispatch ^:flush-dom (assoc event :flushed? true)} ;; <-- adding to the map
;; step 2 - hog the CPU
{:db (do-some-cpu-intensive-task db)})))
Notice that the event changes over time, in two steps. And that ability to change makes the design an easy one.
And, yes, this is a weak example because it simple. It could be achieved via vector events fairly easily. Maps come into their own with other, more complicated async situations like HTTP operations. More on all that soon, once we get this stage done.