2016-08-19 05:32:51 +00:00
|
|
|
## Coeffects
|
2016-08-15 01:56:39 +00:00
|
|
|
|
|
|
|
This tutorial explains `coeffects`.
|
|
|
|
|
2016-08-17 08:53:14 +00:00
|
|
|
It explains what they are, how they can be "injected", and how
|
2016-08-16 02:30:01 +00:00
|
|
|
to manage them in tests.
|
2016-08-15 08:11:06 +00:00
|
|
|
|
2016-11-06 20:12:24 +00:00
|
|
|
<!-- 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
|
|
|
|
|
|
|
|
- [What Are They?](#what-are-they)
|
|
|
|
- [An Example](#an-example)
|
|
|
|
- [How We Want It](#how-we-want-it)
|
|
|
|
- [Abracadabra](#abracadabra)
|
|
|
|
- [Which Interceptors?](#which-interceptors)
|
|
|
|
- [`inject-cofx`](#inject-cofx)
|
|
|
|
- [More `inject-cofx`](#more-inject-cofx)
|
|
|
|
- [Meet `reg-cofx`](#meet-reg-cofx)
|
|
|
|
- [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)
|
|
|
|
|
|
|
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
2016-08-15 01:56:39 +00:00
|
|
|
|
|
|
|
### What Are They?
|
|
|
|
|
2016-08-16 02:30:01 +00:00
|
|
|
`coeffects` are the data resources that an event handler needs
|
|
|
|
to perform its computation.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 02:30:01 +00:00
|
|
|
Because the majority of event handlers only need `db` and
|
|
|
|
`event`, there's a specific registration function, called `reg-event-db`,
|
2016-12-08 23:34:08 +00:00
|
|
|
which delivers ONLY these two coeffects as arguments to an event
|
2016-08-16 02:30:01 +00:00
|
|
|
handler, making this common case easy to program.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 02:30:01 +00:00
|
|
|
But sometimes an event handler needs other data inputs
|
|
|
|
to perform its computation. Things like a random number, or a GUID,
|
2016-08-27 02:41:26 +00:00
|
|
|
or the current datetime. Perhaps it needs access to a
|
2016-08-15 12:54:23 +00:00
|
|
|
DataScript connection.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
|
|
|
|
2016-08-15 12:54:23 +00:00
|
|
|
### An Example
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 14:08:30 +00:00
|
|
|
This handler obtains data directly from LocalStore:
|
2016-08-15 01:56:39 +00:00
|
|
|
```clj
|
2016-08-16 02:30:01 +00:00
|
|
|
(reg-event-db
|
2016-08-15 08:11:06 +00:00
|
|
|
:load-defaults
|
2016-08-19 05:32:51 +00:00
|
|
|
(fn [db _]
|
2016-08-15 01:56:39 +00:00
|
|
|
(let [val (js->clj (.getItem js/localStorage "defaults-key"))] ;; <-- Problem
|
|
|
|
(assoc db :defaults val))))
|
|
|
|
```
|
|
|
|
|
2016-08-27 02:41:26 +00:00
|
|
|
This works, but there's a cost.
|
2016-08-17 08:53:14 +00:00
|
|
|
|
2016-08-16 14:08:30 +00:00
|
|
|
Because it has directly accessed LocalStore, this event handler is not
|
2016-08-27 02:41:26 +00:00
|
|
|
pure, and impure functions cause well-documented paper cuts.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 02:30:01 +00:00
|
|
|
### How We Want It
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-27 02:41:26 +00:00
|
|
|
Our goal in this tutorial will be to rewrite this event handler so
|
|
|
|
that it __only__ uses data from arguments. This will take a few steps.
|
2016-08-16 02:30:01 +00:00
|
|
|
|
2016-12-02 19:16:35 +00:00
|
|
|
The first is that we switch to
|
2016-08-16 14:08:30 +00:00
|
|
|
using `reg-event-fx` (instead of `reg-event-db`).
|
2016-08-16 02:30:01 +00:00
|
|
|
|
2016-08-16 14:08:30 +00:00
|
|
|
Event handlers registered via `reg-event-fx` are slightly
|
2016-08-22 20:56:47 +00:00
|
|
|
different to those registered via `reg-event-db`. `-fx` handlers
|
2016-08-17 08:53:14 +00:00
|
|
|
get two arguments, but the first is not `db`. Instead it
|
|
|
|
is an argument which we will call `cofx` (that's a nice distinct
|
|
|
|
name which will aid communication).
|
2016-08-16 02:30:01 +00:00
|
|
|
|
2016-08-17 08:53:14 +00:00
|
|
|
Previous tutorials showed there's a `:db` key in `cofx`. We
|
2016-08-19 05:32:51 +00:00
|
|
|
now want `cofx` to have other keys and values, like this:
|
2016-08-15 01:56:39 +00:00
|
|
|
```clj
|
2016-08-16 02:30:01 +00:00
|
|
|
(reg-event-fx ;; note: -fx
|
2016-08-15 08:11:06 +00:00
|
|
|
:load-defaults
|
2016-08-16 02:30:01 +00:00
|
|
|
(fn [cofx event] ;; cofx means coeffects
|
|
|
|
(let [val (:local-store cofx) ;; <-- get data from cofx
|
|
|
|
db (:db cofx)] ;; <-- more data from cofx
|
2016-08-15 01:56:39 +00:00
|
|
|
{:db (assoc db :defaults val))})) ;; returns an effect
|
|
|
|
```
|
|
|
|
|
2016-08-16 14:08:30 +00:00
|
|
|
Notice how `cofx` magically contains a `:local-store` key with the
|
2016-08-19 05:32:51 +00:00
|
|
|
right value. Nice! But how do we make this magic happen?
|
2016-08-16 02:30:01 +00:00
|
|
|
|
2016-08-16 14:08:30 +00:00
|
|
|
### Abracadabra
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-27 02:41:26 +00:00
|
|
|
Each time an event handler is executed, a brand new `context`
|
2016-10-13 04:44:29 +00:00
|
|
|
is created, and within that `context` is a brand new `:coeffects`
|
2016-08-27 02:41:26 +00:00
|
|
|
map, which is initially totally empty.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-10-13 04:44:29 +00:00
|
|
|
That pristine `context` value (containing a pristine `:coeffects` map) is threaded
|
2016-08-19 05:32:51 +00:00
|
|
|
through a chain of Interceptors before it finally reaches our event handler,
|
2016-08-16 14:08:30 +00:00
|
|
|
sitting on the end of a chain, itself wrapped up in an interceptor. We know
|
2016-08-15 08:11:06 +00:00
|
|
|
this story well from a previous tutorial.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 02:30:01 +00:00
|
|
|
So, all members of the Interceptor chain have the opportunity to add to `:coeffects`
|
2016-10-13 04:44:29 +00:00
|
|
|
via their `:before` function. This is where `:coeffects` magic happens. This is how
|
|
|
|
new keys can be added to `:coeffects`, so that later our event handler magically finds the
|
2016-08-17 08:53:14 +00:00
|
|
|
right data (like `:local-store`) in its `cofx` argument. It is the Interceptors.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 14:08:30 +00:00
|
|
|
### Which Interceptors?
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-10-13 04:44:29 +00:00
|
|
|
If Interceptors put data in `:coeffects`, then we'll need to add the right ones
|
2016-08-17 08:53:14 +00:00
|
|
|
when we register our event handler.
|
2016-08-15 08:11:06 +00:00
|
|
|
|
2016-08-17 08:53:14 +00:00
|
|
|
Something like this (this handler is the same as before, except for one detail):
|
2016-08-15 01:56:39 +00:00
|
|
|
```clj
|
|
|
|
(reg-event-fx
|
2016-08-15 08:11:06 +00:00
|
|
|
:load-defaults
|
2016-08-16 02:30:01 +00:00
|
|
|
[ (inject-cofx :local-store "defaults-key") ] ;; <-- this is new
|
|
|
|
(fn [cofx event]
|
|
|
|
(let [val (:local-store cofx)
|
|
|
|
db (:db cofx)]
|
2016-08-15 01:56:39 +00:00
|
|
|
{:db (assoc db :defaults val))}))
|
|
|
|
```
|
|
|
|
|
2016-08-27 02:41:26 +00:00
|
|
|
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 itself then goes on to be the first argument
|
2016-08-19 05:32:51 +00:00
|
|
|
to our event handler (`cofx`).
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 02:30:01 +00:00
|
|
|
### `inject-cofx`
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-19 18:47:37 +00:00
|
|
|
`inject-cofx` is part of the re-frame API.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 02:30:01 +00:00
|
|
|
It is a function which returns an Interceptor whose `:before` function loads
|
2016-10-13 04:44:29 +00:00
|
|
|
a key/value pair into a `context's` `:coeffects` map.
|
2016-08-16 02:30:01 +00:00
|
|
|
|
|
|
|
`inject-cofx` takes either one or two arguments. The first is always the `id` of the coeffect
|
|
|
|
required (called a `cofx-id`). The 2nd is an optional addition value.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 02:30:01 +00:00
|
|
|
So, in the case above, the `cofx-id` was `:local-store` and the additional value
|
2016-08-16 14:08:30 +00:00
|
|
|
was "defaults-key" which was presumably the LocalStore key.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 02:30:01 +00:00
|
|
|
### More `inject-cofx`
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-17 08:53:14 +00:00
|
|
|
Here's some other usage examples:
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 02:30:01 +00:00
|
|
|
- `(inject-cofx :random-int 10)`
|
|
|
|
- `(inject-cofx :guid)`
|
|
|
|
- `(inject-cofx :now)`
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-17 08:53:14 +00:00
|
|
|
I could create an event handler which has access to 3 coeffects:
|
2016-08-15 08:11:06 +00:00
|
|
|
```clj
|
|
|
|
(reg-event-fx
|
|
|
|
:some-id
|
2016-08-16 02:30:01 +00:00
|
|
|
[(inject-cofx :random-int 10) (inject-cofx :now) (inject-cofx :local-store "blah")] ;; 3
|
|
|
|
(fn [cofx _]
|
|
|
|
... in here I can access cofx's keys :now :local-store and :random-int))
|
2016-08-15 01:56:39 +00:00
|
|
|
```
|
|
|
|
|
2016-08-19 05:32:51 +00:00
|
|
|
But that's probably just greedy, and not very useful.
|
2016-08-15 12:54:23 +00:00
|
|
|
|
2016-08-17 08:53:14 +00:00
|
|
|
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`?
|
|
|
|
Each `cofx-id` requires a different action.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-15 08:11:06 +00:00
|
|
|
### Meet `reg-cofx`
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-17 08:53:14 +00:00
|
|
|
This function is also part of the re-frame API.
|
|
|
|
|
2016-10-06 04:34:06 +00:00
|
|
|
It allows you to associate a `cofx-id` (like `:now` or `:local-store`) with a
|
2016-08-17 08:53:14 +00:00
|
|
|
handler function that injects the right key/value pair.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-16 09:00:56 +00:00
|
|
|
The function you register will be passed two arguments:
|
2016-08-19 05:32:51 +00:00
|
|
|
- a `:coeffects` map (to which it should add a key/value pair), and
|
2016-08-17 08:53:14 +00:00
|
|
|
- optionally, the additional value supplied to `inject-cofx`
|
|
|
|
and it is expected to return a modified `:coeffects` map.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-19 05:32:51 +00:00
|
|
|
### Example Of `reg-cofx`
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-19 05:32:51 +00:00
|
|
|
Above, we wrote an event handler that wanted `:now` data to be available. Here
|
2016-08-16 02:30:01 +00:00
|
|
|
is how a handler could be registered for `:now`:
|
2016-08-15 08:11:06 +00:00
|
|
|
```clj
|
2016-08-17 08:53:14 +00:00
|
|
|
(reg-cofx ;; registration function
|
2016-08-15 08:11:06 +00:00
|
|
|
:now ;; what cofx-id are we registering
|
2016-08-16 09:00:56 +00:00
|
|
|
(fn [coeffects _] ;; second parameter not used in this case
|
|
|
|
(assoc coeffects :now (js.Date.)))) ;; add :now key, with value
|
2016-08-15 08:11:06 +00:00
|
|
|
```
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-19 05:32:51 +00:00
|
|
|
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:
|
2016-08-15 08:11:06 +00:00
|
|
|
```clj
|
|
|
|
(reg-cofx ;; new registration function
|
|
|
|
:local-store
|
2016-08-16 02:30:01 +00:00
|
|
|
(fn [coeffects local-store-key]
|
2016-08-15 08:11:06 +00:00
|
|
|
(assoc coeffects
|
|
|
|
:local-store
|
2016-09-05 00:42:58 +00:00
|
|
|
(js->clj (.getItem js/localStorage local-store-key)))))
|
2016-08-15 08:11:06 +00:00
|
|
|
```
|
2016-08-15 01:56:39 +00:00
|
|
|
|
|
|
|
|
2016-08-19 05:32:51 +00:00
|
|
|
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.
|
2016-08-15 12:54:23 +00:00
|
|
|
|
2016-08-19 05:32:51 +00:00
|
|
|
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`.
|
2016-08-15 08:11:06 +00:00
|
|
|
|
|
|
|
### Secret Interceptors
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-15 08:11:06 +00:00
|
|
|
In a previous tutorial we learned that `reg-events-db`
|
2016-12-17 08:14:00 +00:00
|
|
|
and `reg-events-fx` add Interceptors to the front of any chain
|
2016-08-19 05:32:51 +00:00
|
|
|
during registration. We found they inserted an Interceptor called `do-fx`.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-17 08:53:14 +00:00
|
|
|
I can now reveal that
|
2016-08-16 09:00:56 +00:00
|
|
|
they also add `(inject-cofx :db)` at the front of each chain.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-19 05:32:51 +00:00
|
|
|
Guess what that injects into the `:coeffects` of every event handler? This is how `:db`
|
|
|
|
is always available to event handlers.
|
2016-08-16 09:00:56 +00:00
|
|
|
|
2016-08-17 08:53:14 +00:00
|
|
|
Okay, so that was the last surprise. Now you know everything.
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-19 05:32:51 +00:00
|
|
|
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.
|
|
|
|
|
|
|
|
|
2016-08-15 08:11:06 +00:00
|
|
|
### Testing
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-19 18:47:37 +00:00
|
|
|
During testing, you may want to stub out certain coeffects.
|
2016-08-16 09:00:56 +00:00
|
|
|
|
|
|
|
You may, for example, want to test that an event handler works
|
2016-08-19 05:32:51 +00:00
|
|
|
using a specific `now`, not a true random number.
|
2016-08-17 08:53:14 +00:00
|
|
|
|
|
|
|
In your test, you'd mock out the cofx handler:
|
2016-08-30 17:07:11 +00:00
|
|
|
```clj
|
2016-08-17 08:53:14 +00:00
|
|
|
(reg-cofx
|
2016-08-19 05:32:51 +00:00
|
|
|
:now
|
2016-08-17 08:53:14 +00:00
|
|
|
(fn [coeffects _]
|
2016-08-19 05:32:51 +00:00
|
|
|
(assoc coeffects :now (js/Date. 2016 1 1))) ;; now was then
|
|
|
|
```
|
|
|
|
|
2016-08-19 18:47:37 +00:00
|
|
|
If your test does alter registered coeffect handlers, and you are using `cljs.test`,
|
2016-08-19 05:32:51 +00:00
|
|
|
then you can use a `fixture` to restore all coeffects at the end of your test:
|
|
|
|
```clj
|
2016-08-24 00:44:21 +00:00
|
|
|
(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-17 08:53:14 +00:00
|
|
|
```
|
|
|
|
|
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-17 08:53:14 +00:00
|
|
|
|
|
|
|
### The 5 Point Summary
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-17 08:53:14 +00:00
|
|
|
In note form:
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-08-17 08:53:14 +00:00
|
|
|
1. Event handlers should only source data from their arguments
|
|
|
|
2. We want to "inject" required data into the first, cofx argument
|
|
|
|
3. We use the `(inject-cofx :key)` interceptor in registration of the event handler
|
|
|
|
4. It will look up the registered cofx handler for that `:key` to do the injection
|
|
|
|
5. We must have previously registered a cofx handler via `reg-cofx`
|
|
|
|
|
2016-08-15 01:56:39 +00:00
|
|
|
|
2016-11-06 20:12:24 +00:00
|
|
|
***
|
|
|
|
|
2016-08-28 15:40:00 +00:00
|
|
|
Previous: [Effects](Effects.md)
|
2016-08-28 15:50:15 +00:00
|
|
|
Up: [Index](README.md)
|
2016-08-28 15:40:00 +00:00
|
|
|
Next: [Basic App Structure](Basic-App-Structure.md)
|