re-frame/docs/EffectfulEvents.md

372 lines
13 KiB
Markdown
Raw Normal View History

2016-08-10 23:01:39 +00:00
This tutorial shows you how to implement pure event handlers that side-effect.
Yes, a surprising claim.
### Events Happen
Events "happen" when they are dispatched.
2016-08-14 04:42:32 +00:00
So, this makes an event happen:
2016-08-10 23:01:39 +00:00
```clj
(dispatch [:set-flag true])
```
Events are normally triggered by an external agent: the user clicks a button, or a server-pushed
message arrives on a websocket.
### Handling The Happening
Once dispatched, an event must be "handled". It must be processed, actioned.
Events are mutative by nature. If your application is in one state before an
event is processed, it will be in a different state afterwards.
And that state change is very desirable. Without the state change our
application can't incorporate that button click, or the newly arrived
websocket message. Without mutation, the apps just sits there, stuck.
2016-08-14 04:42:32 +00:00
State change is how the application "moves forward" - how it does its job. Useful!
2016-08-10 23:01:39 +00:00
On the other hand, control logic and state mutation tend to be the most
complex and error prone of part of an app.
### Your Handling
To help wrangle this potential complexity, re-frame's introduction
provided you with a simple programming model.
It said you should call `reg-event-db` to associate an event id,
with a function to do the handling:
```clj
2016-08-14 04:42:32 +00:00
(re-frame.core/reg-event-db ;; <-- call this to register handlers
:set-flag ;; this is an event id
(fn [db [_ new-value] ;; this function does the handling
2016-08-10 23:01:39 +00:00
(assoc db :flag new-value)))
```
2016-08-14 04:42:32 +00:00
The function you register, handles events with a given `id`.
2016-08-10 23:01:39 +00:00
2016-08-14 04:42:32 +00:00
And that handler `fn` is expected to be pure. Given the
value in `app-db` as the first argument, and the event (vector)
as the second argument, it is expected to provide a new value for `app-db`.
2016-08-10 23:01:39 +00:00
Data in, a computation and data out. Pure.
### 90% Solution
This paradigm provides a lovely solution 90% of the time, but there are times
2016-08-14 04:42:32 +00:00
when it isn't enough.
2016-08-10 23:01:39 +00:00
Here's an example from the messy 10%. To get its job done, this handler has to side effect:
```clj
(reg-event-db
:my-event
(fn [db [_ bool]]
(dispatch [:do-something-else 3]) ;; oops, side-effect
(assoc db :send-spam new-val)))
```
2016-08-14 04:42:32 +00:00
That `dispatch` queues up another event to be processed. It changes the world.
2016-08-10 23:01:39 +00:00
Just to be clear, this code works. The handler returns a new version of `db`, so tick,
and that `dispatch` will itself be "handled" asynchronously
2016-08-14 04:42:32 +00:00
very shortly after this handler finishes, double tick.
2016-08-10 23:01:39 +00:00
So, you can "get away with it". But it ain't pure.
And here's more carnage:
```clj
(reg-event-db
: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)))
```
Again, this approach will work. But that dirty great big side-effect doesn't come
for free. Its like a muddy monster truck has shown up in our field of white tulips.
### Bad, Why?
The moment we stop writing pure functions there are well documented
consequences:
1. Cogitative load for the function's later readers goes up because they can no longer reason locally.
2. Testing becomes more difficult and involves "mocking". How do we test that the http GET above is
using the right URL? "mocking" should be is mocked. It is a code smell.
3. And event replay-ability is lost.
2016-08-14 04:42:32 +00:00
Regarding the 3rd point above, a re-frame application proceeds step by step, like a reduce. From the README:
2016-08-10 23:01:39 +00:00
> 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, inserting extra events into it, etc, which ruins the process.
### The Other Problem
2016-08-14 04:42:32 +00:00
And there's another purity problem:
2016-08-10 23:01:39 +00:00
```clj
(reg-event-db
:load-localstore
(fn [db _]
2016-08-14 04:42:32 +00:00
(let [val (js->clj (.getItem js/localStorage "defaults-key"))] ;; <-- Problem
2016-08-11 13:24:39 +00:00
(assoc db :defaults val))))
2016-08-10 23:01:39 +00:00
```
It sources data from LocalStore.
So this handler has no side effect - it doesn't need to change the world - __but__ it does
2016-08-11 13:24:39 +00:00
need to source data from somewhere other than its arguments - from somewhere
2016-08-10 23:01:39 +00:00
outside of app-db or the event.
2016-08-14 04:42:32 +00:00
So, it isn't a pure function, and that leads to the normal problems.
2016-08-10 23:01:39 +00:00
### Effects And Coeffects
So there are [two concepts at play here](http://tomasp.net/blog/2014/why-coeffects-matter/):
- **Effects** - what your event handler does to the world (aka side-effects)
2016-08-14 04:42:32 +00:00
- **Coeffects** - the data your event handler requires from the world in order to do its computation (aka [side-causes](http://blog.jenkster.com/2015/12/what-is-functional-programming.html))
2016-08-10 23:01:39 +00:00
2016-08-11 13:24:39 +00:00
We'll need a solution for both.
2016-08-10 23:01:39 +00:00
### Why Does This Happen?
It is inevitable that 10% of your event handlers have effects and coeffects.
They have to implement the control logic of your re-frame app, and
that means dealing with the outside, mutative world of servers, databases,
windows.location, LocalStore, cookies, etc.
2016-08-11 13:24:39 +00:00
There's just no getting away from living in a mutative world, er,
which sounds ominous. Is that it? Are we doomed to impurity?
2016-08-10 23:01:39 +00:00
Well, luckily a small twist in the tale makes a profound difference. We
2016-08-14 04:42:32 +00:00
will look at side-effects first. Instead of creating event handlers
2016-08-10 23:01:39 +00:00
which *do side-effects*, we'll instead get them to *cause side-effects*.
### Doing vs Causing
2016-08-11 13:24:39 +00:00
Above, I proudly claimed that this `fn` event handler was pure:
2016-08-10 23:01:39 +00:00
```clj
(reg-event-db
:my-event
(fn [db _]
2016-08-14 04:42:32 +00:00
(assoc db :flag true)))
2016-08-10 23:01:39 +00:00
```
Takes a `db` value, computes and returns a `db` value. No coeffects or effects. Yep, that's Pure!
2016-08-14 04:42:32 +00:00
Yes, all true, but ... this purity is only possible because re-frame is doing
2016-08-10 23:01:39 +00:00
the necessary side-effecting.
Wait on. What "necessary side-effecting"?
2016-08-14 04:42:32 +00:00
Well, application state is stored in `app-db`, right? And it is a ratom. And after
2016-08-10 23:01:39 +00:00
each event handler runs, it must be `reset!` to the newly returned
2016-08-14 04:42:32 +00:00
value. That, right there, is the "necessary side effecting".
2016-08-10 23:01:39 +00:00
2016-08-11 13:24:39 +00:00
We get to live in our ascetic functional world because re-frame is
2016-08-14 04:42:32 +00:00
looking after the "necessary side-effects" on `app-db`.
2016-08-10 23:01:39 +00:00
### Et tu, React?
Turns out it's the same pattern with Reagent/React.
2016-08-14 04:42:32 +00:00
We get to write a nice pure component, like:
2016-08-10 23:01:39 +00:00
```clj
(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
2016-08-14 04:42:32 +00:00
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.
2016-08-10 23:01:39 +00:00
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.
2016-08-14 04:42:32 +00:00
Let's use this pattern to solve the side-effecting event-handler problem.
2016-08-10 23:01:39 +00:00
### The Two Step 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.
### Step 1 Of Plan
So, how would it look if event handlers returned side-effects, declaratively, in data?
Here is an impure, side effecting handler:
```clj
(reg-event-db
: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:
```clj
(reg-event-fx ;; <1>
:my-event
(fn [{:keys [db]} [_ a]] ;; <2>
{:db (assoc db :flag true) ;; <3>
:dispatch [:do-something-else 3]}))
```
Notes: <br>
2016-08-14 04:42:32 +00:00
*<1>* we're using `reg-event-fx` instead of `reg-event-db` to register (that's `-db` vs `-fx`) <br>
*<2>* the first parameter is no longer just `db`. It is a map from which
2016-08-10 23:01:39 +00:00
[we are destructuring db](http://clojure.org/guides/destructuring). Ie.
it is a map which contains a `:db` key. <br>
*<3>* The handler is returning a data structure (map) which describes two side-effects:
- a change to application state, via the `:db` key
- a further event, via the `:dispatch` key
Above, the impure handler **did** a `dispatch` side-effect, while the pure handler **described**
a `dispatch` side-effect.
### Another Example
The impure way:
```clj
(reg-event-db
: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:
```clj
(reg-event-fx
:my-event
(fn [{:keys [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!).
More on side effects in a minute, but let's double back to coeffects.
### The Coeffects
So far we've written our new style `-fx handlers like this:
```clj
(reg-event-fx
:my-event
(fn [{:keys [db]} event] ;; <-- destructuring to get db
{ ... }))
```
2016-08-14 04:42:32 +00:00
It is now time to name that first argument:
2016-08-10 23:01:39 +00:00
```clj
(reg-event-fx
:my-event
2016-08-14 04:42:32 +00:00
(fn [coeffects event] ;; <--- thy name be coeefects
2016-08-10 23:01:39 +00:00
{ ... }))
```
When you use the `-fx` form of registration, the first argument of your handler will be a coeffects map.
In that map will be the complete set of "inputs" required by your function. The complete
2016-08-14 04:42:32 +00:00
set of computational resources (data) needed to perform its computation. But how?
I'll explain in an upcoming tutorial, I promise, but for the moment, take it as a magical given.
2016-08-10 23:01:39 +00:00
2016-08-14 04:42:32 +00:00
One of the keys in `coeffects` will likely be `:db` and that will be the value of `app-db`.
2016-08-10 23:01:39 +00:00
Remember this impure handler from before:
```clj
(reg-event-db ;; a -db registration
:load-localstore
(fn [db _] ;; db first argument
(let [defaults (js->clj (.getItem js/localStorage "defaults-key"))] ;; <-- Eeek
(assoc db :defaults defaults))))
```
We'd now rewrite that as a pure handler, like this:
```clj
(reg-event-fx ;; notice the -fx
:load-localstore
(fn [coeffect _] ;; coeffect is a map containing inputs
(let [defaults (:defaults-key coeffect)] ;; <-- use it here
2016-08-14 04:42:32 +00:00
{:db (assoc (:db coeffects) :defaults defaults)}))) ;; returns effects map
2016-08-10 23:01:39 +00:00
```
So, by some magic, not yet revealed, LocalStore will be queried before
this handler runs and the required value from it will be placed into
`coeffects` under the key `:localstore` for the handler to use.
2016-08-14 04:42:32 +00:00
That process leaves the handler itself pure because it only sources data from arguments.
2016-08-10 23:01:39 +00:00
### Variations On A Theme
`-db` handlers and `-fx` handlers are conceptually the same. They only differ numerically.
`-db` handlers take ONE coeeffect called `db`, and they return only ONE effect (db again).
Whereas `-fx` handlers take potentially MANY coeffects (a map of them) and they return
potentially MANY effects (a map of them). So, One vs Many.
Just to be clear, the following two handlers achieve exactly the same thing:
```clj
(reg-event-db
:set-flag
(fn [db [_ new-value]
(assoc db :flag new-value)))
```
and
```clj
(reg-event-fx
:set-flag
(fn [context [_ new-value]
{:db (assoc (:db context) :flag new-value)}))
```
2016-08-14 04:42:32 +00:00
Obviously the `-db` variation is simpler and you'd use it whenever you
can. The `-fx` version is more flexible, so it will sometimes have its place.
2016-08-10 23:01:39 +00:00
### Summary
2016-08-14 04:42:32 +00:00
90% of the time, simple `-db` handlers are the right tool to use.
2016-08-10 23:01:39 +00:00
But about 10% of the time, our handlers need additional inputs (coeffecs) or they need to
cause additional side-effects (effects). That's when you reach for `-fx` handlers.
2016-08-15 01:56:03 +00:00
`-fx` handlers allow us to return effects, declaratively in data.
2016-08-14 04:42:32 +00:00
In the next tutorial, we'll shine a light on `interceptors` which are
the mechanism by which event handlers are executed. That knowledge will give us a springboard
to more deeply understand coeffects and effects. We'll soon be writing our own.
2016-08-10 23:01:39 +00:00