re-frame/docs/Effects.md

299 lines
9.8 KiB
Markdown
Raw Permalink Normal View History

2016-08-19 05:32:51 +00:00
## Effects
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.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<!-- ## Table Of Contents -->
<!-- - [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) -->
<!-- - [Effects With No Data](#effects-with-no-data) -->
<!-- - [Testing And Noops](#testing-and-noops) -->
<!-- - [Summary](#summary) -->
<!-- - [External Effects](#external-effects) -->
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
2016-08-12 13:47:20 +00:00
### Where Effects Come From
2016-08-19 05:32:51 +00:00
When an event handler is registered via `reg-event-fx`, it must return effects.
2016-08-12 13:47:20 +00:00
Like this:
2016-08-12 13:47:20 +00:00
```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 [cofx [_ a]] ;; 1st argument is coeffects, instead of db
{:db (assoc (:db cofx) :flag a)
2016-08-12 13:47:20 +00:00
:dispatch [:do-something-else 3]})) ;; return effects
```
`-fx` handlers return a description of the side-effects required, and that description is a map.
2016-08-12 13:47:20 +00:00
### The Effects Map
2016-08-12 13:47:20 +00:00
An effects map contains instructions.
Each key/value pair in the map is one instruction - the `key` uniquely identifies
the particular side effect required, and the `value` for that `key` provides
further data. The structure of `value` is different for each side-effect.
2016-08-12 13:47:20 +00:00
Here's the two instructions from the example above:
```clj
2016-08-16 14:08:30 +00:00
{:db (assoc db :flag a) ;; side effect on app-db
:dispatch [:do-something-else 3]} ;; dispatch this event
2016-08-12 13:47:20 +00:00
```
2016-08-19 05:32:51 +00:00
The `:db` `key` instructs that "app-db" should be `reset!` to the
`value` supplied.
2016-08-19 05:32:51 +00:00
And the `: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`.
And so on. And so on. Which brings us to a problem.
2016-08-13 06:11:11 +00:00
### Infinite Effects
2016-08-12 13:47:20 +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.
What if you use PostgreSQL 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
X, Y or Z?
The list of effects is long and varied, with everyone needing to use a
2016-08-16 14:08:30 +00:00
different combination.
2016-08-12 13:47:20 +00:00
So effect handling has to be extensible. You need 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
re-frame provides a function `reg-fx` through which you can register
2016-08-15 01:56:03 +00:00
your own `Effect Handlers`.
2016-08-12 13:47:20 +00:00
Use it like this:
```clj
(reg-fx ;; <-- registration function
:butterfly ;; <1>
2016-08-12 13:47:20 +00:00
(fn [value] ;; <2>
...
))
```
__<1>__ the key for the effect. When later an effects map contains
2016-08-16 14:08:30 +00:00
the key `:butterfly`, the function we are registering will be used to action it. <br>
2016-08-12 13:47:20 +00:00
__<2>__ the function which actions the side effect. Later, it will be called
with one argument - the value in the effects map, for this key.
2016-08-12 13:47:20 +00:00
2016-08-19 05:32:51 +00:00
So, if an event handler returned these two effects:
2016-08-12 13:47:20 +00:00
```clj
{:dispatch [:save-maiden 42]
2016-08-19 05:32:51 +00:00
:butterfly "Flapping"} ;; butterfly effect, but no chaos !!
2016-08-12 13:47:20 +00:00
```
Then the function we registered for `:butterfly` would be called to handle
2016-08-15 08:11:28 +00:00
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"
2016-08-19 05:32:51 +00:00
- and the function registered is an "effect handler".
So re-frame has both `event` handlers and `effect` handlers and they are
2016-08-28 00:07:46 +00:00
different, despite them both starting with `e` and ending in `t`!!
2016-08-12 13:47:20 +00:00
### Writing An Effect Handler
A word of advice - make them as simple as possible, and then
2016-08-19 05:32:51 +00:00
simplify them further. You don't want them containing any fancy logic.
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
`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
2016-08-15 08:11:28 +00:00
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-12-17 08:00:45 +00:00
In my defence, here's the built-in 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-28 00:07:24 +00:00
So, yeah, simple ... and, because of it, I can almost guarantee there's no bug in ... bang, crash, smoke, flames.
2016-08-12 13:47:20 +00:00
> Note: the return value of an effect handler is ignored.
### :db Not Always Needed
An effects map does not need to include the `effect key` `:db`.
It is perfectly valid for an event handler
to not change `app-db`.
In fact, it is perfectly valid for an event handler to return
an effects map of `{}`. Slightly puzzling, but not a problem.
2016-08-14 04:42:32 +00:00
### What Makes This Work?
A silently inserted interceptor.
Whenever you register an event handler via __either__ `reg-event-db`
or `reg-event-fx`, an interceptor, cunningly named `do-fx`,
is inserted at the beginning of the chain.
2016-08-14 04:42:32 +00:00
Example: if your event handler registration looked like this:
```clj
(reg-event-fx
:some-id
[debug (path :right)] ;; <-- two interceptors, apparently
(fn [cofx _]
2016-08-14 04:42:32 +00:00
{}) ;; <-- imagine returned effects here
```
While it might look like you have registered with 2 interceptors,
`reg-event-fx` will make it 3:
```clj
[do-fx debug (path :right)]
```
It silently inserts `do-fx` at the front, and this is a good thing.
2016-08-19 05:32:51 +00:00
The placement of `do-fx` at the beginning of the interceptor chain means
its `:after` function would be the final act when the chain is executed
2016-08-14 04:42:32 +00:00
(forwards and then backwards, as described in the Interceptor Tutorial).
In this final act, the `:after` function extracts `:effects` from `context`
and simply iterates across the key/value pairs it contains, calling the
2016-08-19 05:32:51 +00:00
registered "effect handlers" for each.
> For the record, the FISA Court requires that we deny all claims
> that `do-fx` is secretly injected NSA surveillance-ware. <br>
> 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
your own version of the `do-fx` interceptor at the front
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.
`do-fx` does not currently provide you with control over the order in
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
*Note:* if you feel you need ordering, then please
open an issue and explain the usecase. The current absence of
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
*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
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
2016-08-15 08:11:28 +00:00
Some effects have no associated data:
```clj
(reg-event-fx
:some-id
(fn [coeffect _]
2016-08-15 08:11:28 +00:00
{:exit-fullscreen nil})) ;; <--- no data, use a nil
```
2016-08-16 14:08:30 +00:00
In these cases, although it looks odd, just supply `nil` as the value for this key.
2016-08-15 08:11:28 +00:00
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-19 05:32:51 +00:00
### Testing And Noops
2016-08-12 13:47:20 +00:00
When you are running tests or replaying events, it is sometimes
2016-08-19 05:32:51 +00:00
useful to stub out effects.
2016-08-12 13:47:20 +00:00
This is easily done - you simply register a noop effect handler.
2016-08-12 13:47:20 +00:00
Want to stub out the `:dispatch` effect? Do this:
2016-08-12 13:47:20 +00:00
```clj
(reg-fx
:dispatch
(fn [_] )) ;; a noop
2016-08-12 13:47:20 +00:00
```
2016-08-19 05:32:51 +00:00
If your test does alter registered effect handlers, and you are using `cljs.test`,
then you can use a `fixture` to restore all effect handlers at the end of your test:
2016-08-19 05:32:51 +00:00
```clj
(defn fixture-re-frame
[]
(let [restore-re-frame (atom nil)]
{:before #(reset! restore-re-frame (re-frame.core/make-restore-fn))
:after #(@restore-re-frame)}))
(use-fixtures :each (fixture-re-frame))
2016-08-19 05:32:51 +00:00
```
`re-frame.core/make-restore-fn` creates a checkpoint for re-frame state (including
registered handlers) to which you can return.
2016-08-19 05:32:51 +00:00
### Existing Effect Handlers
Built-in effect handlers are detailed [the API](/docs/API.md) document.
And please review the [External-Resources document](External-Resources.md) for a list of 3rd party Effect Handlers.
### Summary
2016-08-13 06:11:11 +00:00
The 4 Point Summary in note form:
2016-08-13 06:11:11 +00:00
1. Event handlers should only return effect declaratively
2. They return a map like `{:effect1 value1 :effect2 value2}`
3. Keys of this map can refer to builtin effects handlers (see below) or custom ones
4. We use `reg-fx` to register our own effects handlers, built-in ones are already registered
2016-08-13 06:11:11 +00:00
***
Previous: [Interceptors](Interceptors.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Coeffects](Coeffects.md)