More docs work

This commit is contained in:
Mike Thompson 2016-08-19 15:32:51 +10:00
parent 0514e06d0c
commit 48faedb082
2 changed files with 107 additions and 53 deletions

View File

@ -1,4 +1,4 @@
## Introduction
## Effects
About 10% of the time, event handlers need to cause side effects.
@ -21,26 +21,24 @@ make side effects a noop in event replays.
* [What Makes This Work?](#what-makes-this-work-)
* [Order Of Effects?](#order-of-effects-)
* [Effects With No Data](#effects-with-no-data)
* [Noops](#noops)
- [Builtin Effect Handlers](#builtin-effect-handlers)
* [Testing And Noops](#testing-and-noops)
* [Builtin Effect Handlers](#builtin-effect-handlers)
+ [:dispatch-later](#-dispatch-later)
+ [:dispatch](#-dispatch)
+ [:dispatch-n](#-dispatch-n)
+ [:deregister-event-handler](#-deregister-event-handler)
+ [:db](#-db)
- [External Effects](#external-effects)
## Effects
### Where Effects Come From
When an event handler is registered via `reg-event-fx`, it always returns effects.
When an event handler is registered via `reg-event-fx`, it must return effects.
Like this:
```clj
(reg-event-fx ;; -fx registration, not -db registration
:my-event
(fn [coeffects [_ a]] ;; 1st argument is coeffects, instead of db
{:db (assoc (:db coeffects) :flag a)
(fn [cofx [_ a]] ;; 1st argument is coeffects, instead of db
{:db (assoc (:db cofx) :flag a)
:dispatch [:do-something-else 3]})) ;; return effects
```
@ -52,7 +50,7 @@ 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 the `value` is different for each side-effect.
further data. The structure of `value` is different for each side-effect.
Here's the two instructions from the example above:
```cljs
@ -60,10 +58,10 @@ Here's the two instructions from the example above:
:dispatch [:do-something-else 3]} ;; dispatch this event
```
That `:db` `key` instructs that "app-db" should be `reset!` to the
`value` supplied for the `key`.
The `:db` `key` instructs that "app-db" should be `reset!` to the
`value` supplied.
That `:dispatch` `key` instructs that an event should be
And the `:dispatch` `key` instructs that an event should be
dispatched. The `value` is the vector to dispatch.
There's many other possible
@ -76,7 +74,7 @@ And so on. And so on. Which brings us to a problem.
While re-frame supplies a number of builtin effects, the set of
possible effects is open ended.
What if you use Postgress and want an effect which issues mutating
What if you use PostgreSQL and want an effect which issues mutating
queries? Or what if you want to send logs to Logentries or metrics to DataDog.
Or write values to windows.location. And what happens if your database is
X, Y or Z?
@ -107,10 +105,10 @@ the key `:butterfly`, the function we are registering will be used to action it.
__<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.
So, if an event handler returned these effects:
So, if an event handler returned these two effects:
```clj
{:dispatch [:save-maiden 42]
:butterfly "Flapping"} ;; butterfly effect !!
:butterfly "Flapping"} ;; butterfly effect, but no chaos !!
```
Then the function we registered for `:butterfly` would be called to handle
@ -118,12 +116,15 @@ that effect. And it would be called with the parameter "Flapping".
So, terminology:
- `:butterfly` is an "effect key"
- and the function registered is an "effect handler"
- and the function registered is an "effect handler".
So re-frame has `event handlers` and `effect handlers` and they are
different, despite both starting with `e` and ending in `t`!!
### Writing An Effect Handler
A word of advice - make them as simple as possible, and then
simplify them further. You don't want them containing any fancy logic.
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
@ -148,21 +149,20 @@ In my defence, here's the builtin effect handler for `:db`:
(reset! re-frame.db/app-db value)))
```
So, yeah, simple.
So, yeah, simple ... and, because of it, I an almost guarantee there's no bug in ... bang, crash, smoke, flames.
> Note: the return value of an effect handler is ignored.
### :db Not Always Needed
An effects map does not have to include the `effect key` `:db`.
An effects map does not need to include the `effect key` `:db`.
It is perfectly reasonable and valid for an event handler
to return effects which do not include an update to `app-db`.
It is perfectly valid for an event handler
to not change `app-db`.
In fact, it is perfectly okay for an event handler to return
In fact, it is perfectly valid for an event handler to return
an effects map of `{}`. Slightly puzzling, but not a problem.
### What Makes This Work?
A silently inserted interceptor.
@ -186,13 +186,15 @@ While it might look like you have registered with 2 interceptors,
[do-fx debug (path :right)]
```
It silently inserts `do-fx` at the front, and this is a good thing.
The placement of `do-fx` at the beginning of the interceptor chain means
it's `:after` function would be the final act when the chain is executed
(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
registered effect handlers for each.
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>
@ -247,10 +249,10 @@ The associated effect handler would look like:
(.exitFullscreen js/document)))
```
### Noops
### Testing And Noops
When you are running tests or replaying events, it is sometimes
useful to stub out effects.
useful to stub out effects.
This is easily done - you simply register a noop effect handler.
@ -261,7 +263,24 @@ Want to stub out the `:dispatch` effect? Do this:
(fn [_] )) ;; a noop
```
## Builtin Effect Handlers
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:
```clj
(defn re-frame-fixture
[f]
(let [restore-re-frame-fn (re-frame.core/make-restore-fn)]
(try
(f)
(finally (restore-re-frame-fn)))))
(cljs.test/use-fixtures :each re-frame-fixture)
```
`re-frame.core/make-restore-fn` creates a checkpoint for re-frame state (including
registered handlers) to which you can return.
### Builtin Effect Handlers
#### :dispatch-later
@ -317,7 +336,7 @@ usage:
{:db {:key1 value1 key2 value2}}
```
## External Effects
### External Effects
- https://github.com/Day8/re-frame-http-fx (GETs and POSTs)
- https://github.com/Day8/re-frame-forward-events-fx (slightly exotic)

View File

@ -1,4 +1,4 @@
## Introduction
## Coeffects
This tutorial explains `coeffects`.
@ -15,10 +15,11 @@ to manage them in tests.
* [`inject-cofx`](#-inject-cofx-)
* [More `inject-cofx`](#more--inject-cofx-)
* [Meet `reg-cofx`](#meet--reg-cofx-)
* [Examples](#examples)
* [The 5 Point Summary](#the-5-point-summary)
* [Example Of `reg-cofx`](#example-of--reg-cofx-)
* [Another Example Of `reg-cofx`](#another-example-of--reg-cofx-)
* [Secret Interceptors](#secret-interceptors)
* [Testing](#testing)
* [The 5 Point Summary](#the-5-point-summary)
## Coeffects
@ -44,7 +45,7 @@ This handler obtains data directly from LocalStore:
```clj
(reg-event-db
:load-defaults
(fn [coeffects _]
(fn [db _]
(let [val (js->clj (.getItem js/localStorage "defaults-key"))] ;; <-- Problem
(assoc db :defaults val))))
```
@ -69,7 +70,7 @@ is an argument which we will call `cofx` (that's a nice distinct
name which will aid communication).
Previous tutorials showed there's a `:db` key in `cofx`. We
now want `cofx` to have other keys and values:
now want `cofx` to have other keys and values, like this:
```clj
(reg-event-fx ;; note: -fx
:load-defaults
@ -80,7 +81,7 @@ now want `cofx` to have other keys and values:
```
Notice how `cofx` magically contains a `:local-store` key with the
right value. How do we make this magic happen?
right value. Nice! But how do we make this magic happen?
### Abracadabra
@ -88,18 +89,18 @@ Each time an event handler is executed, a brand new `context` is created, and wi
`context` is a brand new `:coeffect` map, which is initially totally empty.
That pristine `context` value (containing a pristine `:coeffect` map) is threaded
through a chain of Interceptors before it is finally handled to our event handler,
through a chain of Interceptors before it finally reaches our event handler,
sitting on the end of a chain, itself wrapped up in an interceptor. We know
this story well from a previous tutorial.
So, all members of the Interceptor chain have the opportunity to add to `:coeffects`
via their `:before` function. This is where `:coeffect` magic happens. This is how
new keys can be added to `:coeffect`, so that later our handler magically finds the
new keys can be added to `:coeffect`, so that later our event handler magically finds the
right data (like `:local-store`) in its `cofx` argument. It is the Interceptors.
### Which Interceptors?
If Interceptors put data in `:coeffect`, then we'll need to add the right ones to
If Interceptors put data in `:coeffect`, then we'll need to add the right ones
when we register our event handler.
Something like this (this handler is the same as before, except for one detail):
@ -113,9 +114,9 @@ Something like this (this handler is the same as before, except for one detail):
{:db (assoc db :defaults val))}))
```
So we've added one Interceptor. It will inject the right key/value pair
into `context's` `:coeffeects` which then goes on to be the first argument
to our event handler (`cofx`)
Look at that - my event handler has a new Interceptor! It is injecting the right key/value pair (`:local-store`)
into `context's` `:coeffeects`, which then goes on to be the first argument
to our event handler (`cofx`).
### `inject-cofx`
@ -148,7 +149,7 @@ I could create an event handler which has access to 3 coeffects:
... in here I can access cofx's keys :now :local-store and :random-int))
```
But that's probably just showing off, and not very useful.
But that's probably just greedy, and not very useful.
And so, to the final piece in the puzzle: how does `inject-cofx`
know what to do when it is given `:now` or `:local-store`?
@ -162,13 +163,13 @@ It allows you associate a`cofx-id` (like `:now` or `:local-store`) with a
handler function that injects the right key/value pair.
The function you register will be passed two arguments:
- a `:coeffects` map (to which it should add key/value pair), and
- a `:coeffects` map (to which it should add a key/value pair), and
- optionally, the additional value supplied to `inject-cofx`
and it is expected to return a modified `:coeffects` map.
### Examples Of `reg-cofx`
### Example Of `reg-cofx`
Above we wrote an event handler that wanted `:now` data to be available. Here
Above, we wrote an event handler that wanted `:now` data to be available. Here
is how a handler could be registered for `:now`:
```clj
(reg-cofx ;; registration function
@ -177,7 +178,17 @@ is how a handler could be registered for `:now`:
(assoc coeffects :now (js.Date.)))) ;; add :now key, with value
```
And there's this example:
The outcome is:
1. because that cofx handler above is now registered for `:now`, I can
2. add an Interceptor to an event handler which
3. looks like `(inject-cofx :now)`
4. which means within that event handler I can access a `:now` value from `cofx`
As a result, my event handler is pure.
### Another Example Of `reg-cofx`
This:
```clj
(reg-cofx ;; new registration function
:local-store
@ -188,38 +199,62 @@ And there's this example:
```
With these two registrations in place, I can now use `(inject-cofx :now)` and
`(inject-cofx :local-store "blah")` in an effect handler's interceptor chain.
With these two registrations in place, I could now use both `(inject-cofx :now)` and
`(inject-cofx :local-store "blah")` in an event handler's interceptor chain.
To put this another way: I can't use `(inject-cofx :blah)` UNLESS I have previously
used `reg-cofx` to register a handler for `:blah`. Otherwise `inject-cofx` doesn't
know how to inject a `:blah`.
### Secret Interceptors
In a previous tutorial we learned that `reg-events-db`
and `reg-events-fx` add Interceptors to front of any chain
during registration.We found they inserted an Interceptor called `do-fx`.
during registration. We found they inserted an Interceptor called `do-fx`.
I can now reveal that
they also add `(inject-cofx :db)` at the front of each chain.
Guess what that injects into the `:coeffects` of every event handler?
Guess what that injects into the `:coeffects` of every event handler? This is how `:db`
is always available to event handlers.
Okay, so that was the last surprise. Now you know everything.
If ever you wanted to use DataScript, instead of an atom-containing-a-map
like `app-db`, you'd replace `reg-event-db` and `reg-event-fx` with your own
registration functions and have them auto insert the DataScript connection.
### Testing
During testing, you may want to mock certain coeffets.
During testing, you may want to stub out certain coeffets.
You may, for example, want to test that an event handler works
using a specific `random number`, not a true random number.
using a specific `now`, not a true random number.
In your test, you'd mock out the cofx handler:
```
(reg-cofx
:random-int
:now
(fn [coeffects _]
(assoc coeffects :random-int 5))) ;; 5 is not very random
(assoc coeffects :now (js/Date. 2016 1 1))) ;; now was then
```
If your test does alter registered coeffect handlers, and you are using `cljs.test`,
then you can use a `fixture` to restore all coeffects at the end of your test:
```clj
(defn re-frame-fixture
[f]
(let [restore-re-frame-fn (re-frame.core/make-restore-fn)]
(try
(f)
(finally (restore-re-frame-fn)))))
(cljs.test/use-fixtures :each re-frame-fixture)
```
`re-frame.core/make-restore-fn` creates a checkpoint for re-frame state (including
registered handlers) to which you can return.
### The 5 Point Summary