re-frame/docs/Coeffects.md

277 lines
9.5 KiB
Markdown
Raw Normal View History

2016-08-19 05:32:51 +00:00
## Coeffects
This tutorial explains `coeffects`.
2016-08-17 08:53:14 +00:00
It explains what they are, how they can be "injected", and how
to manage them in tests.
2016-08-15 08:11:06 +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 -->
### What Are They?
`coeffects` are the data resources that an event handler needs
to perform its computation.
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
handler, making this common case easy to program.
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 12:54:23 +00:00
### An Example
2016-08-16 14:08:30 +00:00
This handler obtains data directly from LocalStore:
```clj
(reg-event-db
2016-08-15 08:11:06 +00:00
:load-defaults
2016-08-19 05:32:51 +00:00
(fn [db _]
(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.
### How We Want It
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-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 14:08:30 +00:00
Event handlers registered via `reg-event-fx` are slightly
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-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:
```clj
(reg-event-fx ;; note: -fx
2016-08-15 08:11:06 +00:00
:load-defaults
(fn [cofx event] ;; cofx means coeffects
(let [val (:local-store cofx) ;; <-- get data from cofx
db (:db cofx)] ;; <-- more data from cofx
{: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 14:08:30 +00:00
### Abracadabra
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-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.
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-16 14:08:30 +00:00
### Which Interceptors?
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):
```clj
(reg-event-fx
2016-08-15 08:11:06 +00:00
:load-defaults
[ (inject-cofx :local-store "defaults-key") ] ;; <-- this is new
(fn [cofx event]
(let [val (:local-store cofx)
db (:db cofx)]
{: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`).
### `inject-cofx`
2016-08-19 18:47:37 +00:00
`inject-cofx` is part of the re-frame API.
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.
`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.
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.
### More `inject-cofx`
2016-08-17 08:53:14 +00:00
Here's some other usage examples:
- `(inject-cofx :random-int 10)`
- `(inject-cofx :guid)`
- `(inject-cofx :now)`
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
[(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-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 08:11:06 +00:00
### Meet `reg-cofx`
2016-08-17 08:53:14 +00:00
This function is also part of the re-frame API.
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.
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-19 05:32:51 +00:00
### Example Of `reg-cofx`
2016-08-19 05:32:51 +00:00
Above, we wrote an event handler that wanted `:now` data to be available. Here
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
(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-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
(fn [coeffects local-store-key]
2016-08-15 08:11:06 +00:00
(assoc coeffects
:local-store
(js->clj (.getItem js/localStorage local-store-key)))))
2016-08-15 08:11:06 +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 08:11:06 +00:00
In a previous tutorial we learned that `reg-events-db`
2016-08-17 08:53:14 +00:00
and `reg-events-fx` add Interceptors to 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-17 08:53:14 +00:00
I can now reveal that
they also add `(inject-cofx :db)` at the front of each chain.
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-17 08:53:14 +00:00
Okay, so that was the last surprise. Now you know everything.
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-19 18:47:37 +00:00
During testing, you may want to stub out certain coeffects.
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:
```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
(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-17 08:53:14 +00:00
In note form:
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`
***
Previous: [Effects](Effects.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Basic App Structure](Basic-App-Structure.md)