2016-08-13 06:11:11 +00:00
|
|
|
## Introduction
|
2016-08-12 13:47:20 +00:00
|
|
|
|
|
|
|
About 10% of the time, event handlers need to cause side effects.
|
|
|
|
|
|
|
|
This tutorial explains how side effects are actioned,
|
|
|
|
how you can create your own side effects, and how you can
|
2016-08-15 01:56:03 +00:00
|
|
|
make side effects a noop in event replays.
|
|
|
|
|
|
|
|
> imperative programming is a big pile of do <br>
|
|
|
|
> -- @stuarthalloway
|
|
|
|
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-13 06:11:11 +00:00
|
|
|
## Table Of Contexts
|
|
|
|
|
|
|
|
- [Introduction](#introduction)
|
|
|
|
- [Table Of Contexts](#table-of-contexts)
|
|
|
|
- [Effects](#effects)
|
|
|
|
* [Where Effects Come From](#where-effects-come-from)
|
|
|
|
* [The Effects Map](#the-effects-map)
|
|
|
|
* [Infinite Effects](#infinite-effects)
|
|
|
|
* [Extensible Side Effects](#extensible-side-effects)
|
|
|
|
* [Writing An Effect Handler](#writing-an-effect-handler)
|
|
|
|
* [:db Not Always Needed](#-db-not-always-needed)
|
|
|
|
* [What Makes This Work?](#what-makes-this-work-)
|
|
|
|
* [Order Of Effects?](#order-of-effects-)
|
2016-08-15 08:11:28 +00:00
|
|
|
* [Effects With No Data](#effects-with-no-data)
|
2016-08-13 06:11:11 +00:00
|
|
|
* [Noops](#noops)
|
2016-08-14 04:42:32 +00:00
|
|
|
- [Builtin Effect Handlers](#builtin-effect-handlers)
|
2016-08-13 06:11:11 +00:00
|
|
|
+ [:dispatch-later](#-dispatch-later)
|
|
|
|
+ [:dispatch](#-dispatch)
|
|
|
|
+ [:dispatch-n](#-dispatch-n)
|
|
|
|
+ [:deregister-event-handler](#-deregister-event-handler)
|
|
|
|
+ [:db](#-db)
|
2016-08-14 04:42:32 +00:00
|
|
|
- [External Effects](#external-effects)
|
2016-08-13 03:18:17 +00:00
|
|
|
|
2016-08-13 06:11:11 +00:00
|
|
|
## Effects
|
2016-08-12 13:47:20 +00:00
|
|
|
### Where Effects Come From
|
|
|
|
|
2016-08-15 01:56:03 +00:00
|
|
|
When an event handler is registered via `reg-event-fx`, it always returns effects.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
|
|
|
Like this:
|
|
|
|
```clj
|
2016-08-15 01:56:03 +00:00
|
|
|
(reg-event-fx ;; -fx registration, not -db registration
|
2016-08-12 13:47:20 +00:00
|
|
|
:my-event
|
|
|
|
(fn [coeffects [_ a]] ;; 1st argument is coeffects, instead of db
|
|
|
|
{:db (assoc (:db coeffects) :flag a)
|
|
|
|
:dispatch [:do-something-else 3]})) ;; return effects
|
|
|
|
```
|
|
|
|
|
|
|
|
`-fx` handlers return a description of the side-effects required, and that description is a map.
|
|
|
|
|
|
|
|
### The Effects Map
|
|
|
|
|
|
|
|
An effects map contains instructions.
|
|
|
|
|
2016-08-15 08:11:28 +00:00
|
|
|
Each key/value pair in the map is one instruction - the `key` uniquely identifies
|
2016-08-13 03:18:17 +00:00
|
|
|
the particular side effect required, and the `value` for that `key` provides
|
2016-08-15 01:56:03 +00:00
|
|
|
further data. The structure of the `value` varies on a per side-effect basis.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-14 04:42:32 +00:00
|
|
|
Here's the two instructions from the example above:
|
2016-08-12 13:47:20 +00:00
|
|
|
```cljs
|
|
|
|
{:db (assoc db :flag a) ;; side effect on app-db
|
|
|
|
:dispatch [:do-something-else 3]} ;; dispatch this event
|
|
|
|
```
|
|
|
|
|
2016-08-15 08:11:28 +00:00
|
|
|
That `:db` `key` instructs that "app-db" should be `reset!` to the
|
|
|
|
`value` supplied for the `key`.
|
2016-08-13 03:18:17 +00:00
|
|
|
|
|
|
|
That `:dispatch` `key` instructs that an event should be
|
2016-08-15 08:11:28 +00:00
|
|
|
dispatched. The `value` is the vector to dispatch.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-14 04:42:32 +00:00
|
|
|
There's many other possible
|
|
|
|
effects, like for example `:dispatch-later` or `:set-local-store`.
|
2016-08-13 03:18:17 +00:00
|
|
|
|
2016-08-12 13:47:20 +00:00
|
|
|
And so on. And so on. Which brings us to a problem.
|
2016-08-13 03:18:17 +00:00
|
|
|
|
2016-08-13 06:11:11 +00:00
|
|
|
### Infinite Effects
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-13 03:18:17 +00:00
|
|
|
While re-frame supplies a number of builtin effects, the set of
|
2016-08-12 13:47:20 +00:00
|
|
|
possible effects is open ended.
|
2016-08-13 03:18:17 +00:00
|
|
|
|
2016-08-12 13:47:20 +00:00
|
|
|
What if you use Postgress and want an effect which issues mutating
|
2016-08-15 01:56:03 +00:00
|
|
|
queries? Or what if you want to send logs to Logentries or metrics to DataDog.
|
2016-08-14 04:42:32 +00:00
|
|
|
Or write values to windows.location. And what happens if your database is
|
2016-08-13 03:18:17 +00:00
|
|
|
X, Y or Z?
|
|
|
|
|
2016-08-15 08:11:28 +00:00
|
|
|
The list of effects is long and varied, with everyone using a
|
|
|
|
different combination of them.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-13 03:18:17 +00:00
|
|
|
So effect handling has to be extensible. You need to a way to define
|
2016-08-14 04:42:32 +00:00
|
|
|
your own side effects.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
|
|
|
### Extensible Side Effects
|
|
|
|
|
2016-08-15 01:56:03 +00:00
|
|
|
re-frame provides a function `reg-fx` through which you can register
|
|
|
|
your own `Effect Handlers`.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
|
|
|
Use it like this:
|
|
|
|
```clj
|
|
|
|
(reg-fx ;; <-- registration function
|
2016-08-13 03:18:17 +00:00
|
|
|
:butterfly ;; <1>
|
2016-08-12 13:47:20 +00:00
|
|
|
(fn [value] ;; <2>
|
|
|
|
...
|
|
|
|
))
|
|
|
|
```
|
2016-08-13 03:18:17 +00:00
|
|
|
|
2016-08-12 13:47:20 +00:00
|
|
|
__<1>__ the key for the effect. When an effects map contains
|
2016-08-15 08:11:28 +00:00
|
|
|
the key `:butterfly`, the registered function will be used to action it. <br>
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-15 08:11:28 +00:00
|
|
|
__<2>__ the function which actions the side effect. It will be called
|
2016-08-14 04:42:32 +00:00
|
|
|
with one argument - the value for this key, in the effects map.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
|
|
|
So, if an event handler returned these effects:
|
|
|
|
```clj
|
2016-08-13 03:18:17 +00:00
|
|
|
{:dispatch [:save-maiden 42]
|
|
|
|
:butterfly "Flapping"} ;; butterfly effect !!
|
2016-08-12 13:47:20 +00:00
|
|
|
```
|
|
|
|
|
2016-08-15 08:11:28 +00:00
|
|
|
Then the function we registered for `:butterfly` would be called to handle
|
|
|
|
that effect. And it would be called with the parameter "Flapping".
|
2016-08-12 13:47:20 +00:00
|
|
|
|
|
|
|
So, terminology:
|
2016-08-14 04:42:32 +00:00
|
|
|
- `:butterfly` is an "effect key"
|
|
|
|
- and the function registered is an "effect handler"
|
2016-08-13 03:18:17 +00:00
|
|
|
|
2016-08-12 13:47:20 +00:00
|
|
|
### Writing An Effect Handler
|
|
|
|
|
2016-08-14 04:42:32 +00:00
|
|
|
A word of advice - make them as simple as possible, and then
|
|
|
|
simplify them further. You don't want them containing any fancy logic.
|
2016-08-13 03:18:17 +00:00
|
|
|
|
2016-08-14 04:42:32 +00:00
|
|
|
Why? Well, because they are all side-effecty they will be a pain
|
|
|
|
to test rigorously. And the combination of fancy logic and limited
|
2016-08-15 08:11:28 +00:00
|
|
|
testing always ends in tears. If not now, later.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
|
|
|
A second word of advice - when you create an effect handler,
|
|
|
|
you also have to design (and document!) the structure of the
|
2016-08-13 03:18:17 +00:00
|
|
|
`value` expected.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-15 08:11:28 +00:00
|
|
|
When you do, realise that you are designing a nano DSL for `value` and try to
|
|
|
|
make that design simple too. If you resist being terse and smart, and instead, favor slightly
|
|
|
|
verbose and obvious, your future self will thank you. Create as little
|
|
|
|
cognitive overhead as possible for the eventual readers of your effectful code.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-15 08:11:28 +00:00
|
|
|
This advice coming from the guy who named effects `fx` ... Oh, the hypocrisy.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-15 08:11:28 +00:00
|
|
|
In my defence, here's the builtin effect handler for `:db`:
|
2016-08-12 13:47:20 +00:00
|
|
|
```clj
|
|
|
|
(reg-fx
|
|
|
|
:db
|
|
|
|
(fn [value]
|
|
|
|
(reset! re-frame.db/app-db value)))
|
|
|
|
```
|
|
|
|
|
2016-08-13 03:18:17 +00:00
|
|
|
So, yeah, simple.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
|
|
|
> Note: the return value of an effect handler is ignored.
|
|
|
|
|
|
|
|
### :db Not Always Needed
|
|
|
|
|
2016-08-13 03:18:17 +00:00
|
|
|
An effects map does not have to include the `effect key` `:db`.
|
|
|
|
|
2016-08-15 08:11:28 +00:00
|
|
|
It is perfectly reasonable and valid for an event handler
|
|
|
|
to return effects which do not include an update to `app-db`.
|
2016-08-13 03:18:17 +00:00
|
|
|
|
2016-08-14 04:42:32 +00:00
|
|
|
In fact, it is perfectly okay for an event handler to return
|
2016-08-15 08:11:28 +00:00
|
|
|
an effects map of `{}`. Slightly puzzling, but not a problem.
|
2016-08-14 04:42:32 +00:00
|
|
|
|
2016-08-13 03:18:17 +00:00
|
|
|
|
|
|
|
### What Makes This Work?
|
|
|
|
|
|
|
|
A silently inserted interceptor.
|
|
|
|
|
2016-08-14 04:42:32 +00:00
|
|
|
Whenever you register an event handler via __either__ `reg-event-db`
|
2016-08-16 03:00:52 +00:00
|
|
|
or `reg-event-fx`, an interceptor, cunningly named `do-fx`,
|
2016-08-14 04:42:32 +00:00
|
|
|
is inserted at the beginning of the chain.
|
2016-08-13 03:18:17 +00:00
|
|
|
|
2016-08-14 04:42:32 +00:00
|
|
|
Example: if your event handler registration looked like this:
|
2016-08-13 03:18:17 +00:00
|
|
|
```clj
|
|
|
|
(reg-event-fx
|
|
|
|
:some-id
|
|
|
|
[debug (path :right)] ;; <-- two interceptors, apparently
|
|
|
|
(fn [coeffect]
|
2016-08-14 04:42:32 +00:00
|
|
|
{}) ;; <-- imagine returned effects here
|
2016-08-13 03:18:17 +00:00
|
|
|
```
|
|
|
|
|
2016-08-14 04:42:32 +00:00
|
|
|
While it might look like you have registered with 2 interceptors,
|
|
|
|
`reg-event-fx` will make it 3:
|
2016-08-13 03:18:17 +00:00
|
|
|
```clj
|
2016-08-16 03:00:52 +00:00
|
|
|
[do-fx debug (path :right)]
|
2016-08-13 03:18:17 +00:00
|
|
|
```
|
|
|
|
|
2016-08-16 03:00:52 +00:00
|
|
|
The placement of `do-fx` at the beginning of the interceptor chain means
|
2016-08-14 04:42:32 +00:00
|
|
|
it's `:after` function would be the final act when the chain is executed
|
|
|
|
(forwards and then backwards, as described in the Interceptor Tutorial).
|
2016-08-13 03:18:17 +00:00
|
|
|
|
|
|
|
In this final act, the `:after` function extracts `:effects` from `context`
|
|
|
|
and simply iterates across the key/value pairs it contains, calling the
|
|
|
|
registered effect handlers for each.
|
|
|
|
|
|
|
|
> For the record, the FISA Court requires that we deny all claims
|
2016-08-16 03:00:52 +00:00
|
|
|
> that `do-fx` is secretly injected NSA surveillance-ware. <br>
|
2016-08-13 03:18:17 +00:00
|
|
|
> We also note that you've been particularly sloppy with your personal
|
|
|
|
> grooming today, including that you forgot to clean your teeth. Again.
|
|
|
|
|
|
|
|
If ever you want to take control of the way effect handling is done,
|
|
|
|
create your own alternative to `reg-event-fx` and, in it, inject
|
2016-08-16 03:00:52 +00:00
|
|
|
your own version of the `do-fx` interceptor at the front
|
2016-08-13 03:18:17 +00:00
|
|
|
of the interceptor chain. It is only a few lines of code.
|
|
|
|
|
|
|
|
|
|
|
|
### Order Of Effects?
|
2016-08-12 13:47:20 +00:00
|
|
|
|
|
|
|
There isn't one.
|
|
|
|
|
2016-08-16 03:00:52 +00:00
|
|
|
`do-fx` does not currently provide you with control over the order in
|
2016-08-12 13:47:20 +00:00
|
|
|
which side effects occur. The `:db` side effect
|
2016-08-15 08:11:28 +00:00
|
|
|
might happen before `:dispatch`, or not. You can't rely on it.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-14 04:42:32 +00:00
|
|
|
*Note:* if you feel you need ordering, then please
|
|
|
|
open an issue and explain the usecase. The current absence of
|
2016-08-15 08:11:28 +00:00
|
|
|
good usecases is the reason ordering isn't implemented. So give
|
|
|
|
us a usercase and we'll revisit, maybe.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-15 08:11:28 +00:00
|
|
|
*Further Note:* if later ordering was needed, it might be handled via
|
|
|
|
metadata on `:effects`. Also, perhaps by allowing `reg-fx` to optionally
|
2016-08-14 04:42:32 +00:00
|
|
|
take two functions:
|
2016-08-15 08:11:28 +00:00
|
|
|
- an effects pre-process fn <-- new. Takes `:effects` returns `:effects`
|
|
|
|
- the effects handler (as already described above).
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-13 03:18:17 +00:00
|
|
|
Anyway, these are all just possibilities. But not needed or implemented yet.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
|
|
|
|
2016-08-15 08:11:28 +00:00
|
|
|
### Effects With No Data
|
|
|
|
|
|
|
|
Some effects have no associated data:
|
|
|
|
```clj
|
|
|
|
(reg-event-fx
|
|
|
|
:some-id
|
|
|
|
(fn [coeffect _]
|
|
|
|
{:exit-fullscreen nil})) ;; <--- no data, use a nil
|
|
|
|
```
|
|
|
|
|
|
|
|
So in these cases, although it looks odd, just supply `nil` as the value for this key.
|
|
|
|
|
|
|
|
The associated effect handler would look like:
|
|
|
|
```clj
|
|
|
|
(reg-fx
|
|
|
|
:exit-fullscreen
|
|
|
|
(fn [_] ;; we don't bother with that nil value
|
|
|
|
(.exitFullscreen js/document)))
|
|
|
|
```
|
|
|
|
|
2016-08-12 13:47:20 +00:00
|
|
|
### Noops
|
|
|
|
|
2016-08-13 03:18:17 +00:00
|
|
|
When you are running tests or replaying events, it is sometimes
|
|
|
|
useful to stub out effects.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-13 03:18:17 +00:00
|
|
|
This is easily done - you simply register a noop effect handler.
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-13 03:18:17 +00:00
|
|
|
Want to stub out the `:dispatch` effect? Do this:
|
2016-08-12 13:47:20 +00:00
|
|
|
```clj
|
|
|
|
(reg-fx
|
2016-08-13 03:18:17 +00:00
|
|
|
:dispatch
|
|
|
|
(fn [_] )) ;; a noop
|
2016-08-12 13:47:20 +00:00
|
|
|
```
|
|
|
|
|
2016-08-13 03:18:17 +00:00
|
|
|
XXX talk about reinstating:
|
|
|
|
- capture return
|
|
|
|
- XXX new feature?
|
2016-08-12 13:47:20 +00:00
|
|
|
|
2016-08-13 03:18:17 +00:00
|
|
|
|
2016-08-14 04:42:32 +00:00
|
|
|
## Builtin Effect Handlers
|
2016-08-13 03:18:17 +00:00
|
|
|
|
|
|
|
#### :dispatch-later
|
|
|
|
|
|
|
|
`dispatch` one or more events after given delays. Expects a collection
|
|
|
|
of maps with two keys: `:ms` and `:dispatch`
|
|
|
|
|
|
|
|
usage:
|
|
|
|
```clj
|
|
|
|
{:dispatch-later [{:ms 200 :dispatch [:event-id "param"]}
|
|
|
|
{:ms 100 :dispatch [:also :this :in :100ms]}]}
|
|
|
|
```
|
|
|
|
|
|
|
|
Which means: in 200ms do this: `(dispatch [:event-id "param"])` and in 100ms ...
|
|
|
|
|
|
|
|
#### :dispatch
|
|
|
|
|
|
|
|
`dispatch` one event. Excepts a single vector.
|
|
|
|
|
|
|
|
usage:
|
|
|
|
```clj
|
|
|
|
{:dispatch [:event-id "param"] }
|
|
|
|
```
|
|
|
|
|
|
|
|
#### :dispatch-n
|
|
|
|
|
|
|
|
`dispatch` more than one event. Expects a collection events.
|
|
|
|
|
|
|
|
usage:
|
|
|
|
```clj
|
|
|
|
{:dispatch-n (list [:do :all] [:three :of] [:these])}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### :deregister-event-handler
|
|
|
|
|
|
|
|
removes a previously registered event handler. Expects either a single id (
|
|
|
|
typically a keyword), or a seq of ids.
|
|
|
|
|
|
|
|
usage:
|
|
|
|
```clj
|
|
|
|
{:deregister-event-handler :my-id)}
|
|
|
|
```
|
|
|
|
or:
|
|
|
|
```clj
|
|
|
|
{:deregister-event-handler [:one-id :another-id]}
|
|
|
|
```
|
|
|
|
|
|
|
|
#### :db
|
|
|
|
|
|
|
|
reset! app-db with a new value. Expects a map.
|
|
|
|
|
|
|
|
usage:
|
|
|
|
```clj
|
|
|
|
{:db {:key1 value1 key2 value2}}
|
|
|
|
```
|
2016-08-13 06:11:11 +00:00
|
|
|
|
2016-08-14 04:42:32 +00:00
|
|
|
## External Effects
|
2016-08-13 06:11:11 +00:00
|
|
|
|
|
|
|
- https://github.com/Day8/re-frame-http-fx (GETs and POSTs)
|
|
|
|
- https://github.com/Day8/re-frame-forward-events-fx (slightly exotic)
|
|
|
|
- https://github.com/Day8/re-frame-async-flow-fx (more complicated)
|
|
|
|
|
|
|
|
Create a PR to include yours in this list.
|
2016-08-15 08:11:28 +00:00
|
|
|
|
|
|
|
XXX maybe put this list into the Wiki, so editable by all.
|