re-frame/docs/coeffects.md

201 lines
6.6 KiB
Markdown
Raw Normal View History

## Introduction
This tutorial explains `coeffects`.
It explains what they are, how they are created, how they help, and how
2016-08-15 08:11:06 +00:00
to manage them in tests. There's also an adults-only moment.
## Table Of Contexts
2016-08-15 08:11:06 +00:00
- [Introduction](#introduction)
- [Table Of Contexts](#table-of-contexts)
- [Coeffects](#coeffects)
* [What Are They?](#what-are-they-)
* [An Example](#an-example)
* [Let's Fix It](#let-s-fix-it)
* [How Are Coeffect Babies Made?](#how-are-coeffect-babies-made-)
* [So, Next Step](#so--next-step)
* [`coeffect` the function](#-coeffect--the-function)
* [Other Example Uses of `coeffects`](#other-example-uses-of--coeffects-)
* [Meet `reg-cofx`](#meet--reg-cofx-)
* [Examples](#examples)
2016-08-15 12:54:23 +00:00
* [The 4 Point Summary](#the-4-point-summary)
2016-08-15 08:11:06 +00:00
* [Secret Interceptors](#secret-interceptors)
* [Testing](#testing)
## Coeffects
### What Are They?
2016-08-15 08:11:06 +00:00
`coeffects` are the input resources that an event handler requires
to perform its computation. By "resources" I mean "data".
Because the majority of event handlers require only `db` and
2016-08-15 08:11:06 +00:00
`event`, there's a registration function, called `reg-event-db`,
which delivers these two coeffects as arguments to an event
handler, making it easy.
But sometimes an event handler needs other inputs
to perform their computation. Things like a random number, or a GUID,
2016-08-15 12:54:23 +00:00
or the current datetime. It might even need access to a
DataScript connection.
2016-08-15 12:54:23 +00:00
### An Example
2016-08-15 08:11:06 +00:00
This handler obtains data directly from LocalStore
```clj
(reg-event-fx
2016-08-15 08:11:06 +00:00
:load-defaults
(fn [coeffects _]
(let [val (js->clj (.getItem js/localStorage "defaults-key"))] ;; <-- Problem
(assoc db :defaults val))))
```
2016-08-15 12:54:23 +00:00
Because it has accessed LocalStore, this event handler is not
2016-08-15 08:11:06 +00:00
pure, which will trigger well documented paper cuts.
2016-08-15 08:11:06 +00:00
### Let's Fix It
2016-08-15 08:11:06 +00:00
Here's how __we'd like to rewrite that handler__. Data should
only come from the `coeffects` argument:
```clj
(reg-event-fx ;; using -fx registration
2016-08-15 08:11:06 +00:00
:load-defaults
(fn [coeffects event] ;; 1st argument is coeffects
2016-08-15 08:11:06 +00:00
(let [val (:local-store coeffects) ;; <-- get value from argument
db (:db coeffects)]
{:db (assoc db :defaults val))})) ;; returns an effect
```
2016-08-15 08:11:06 +00:00
Problem solved? Well, yes, the handler is now pure. But we have a
new problem: how do we arrange for the right `:local-store` value
to be available in `coeffects`.
### How Are Coeffect Babies Made?
2016-08-15 12:54:23 +00:00
Well, when two coeffects love each other very much ... no, stop ... this
2016-08-15 08:11:06 +00:00
is a G-rated framework. Instead ...
Every time an event handler is executed, a new `context` is created, and within that
`context` is a new `:coeffect` map, which is initially totally empty.
That pristine `context` value is then threaded through a chain of Interceptors, and sitting
2016-08-15 08:11:06 +00:00
on the end of chain is the event handler, wrapped up as the final interceptor. We know
this story well from a previous tutorial.
2016-08-15 08:11:06 +00:00
So all members of the Interceptor chain have the opportunity to add to `:coeffects`
via their `:before` function.
### So, Next Step
2016-08-15 08:11:06 +00:00
Armed with that mindset, let's add an interceptor to the registration, which
puts the right localstore value into coeffect.
Here's a sketch:
```clj
(reg-event-fx
2016-08-15 08:11:06 +00:00
:load-defaults
[ (coeffect :local-store "defaults-key") ] ;; <-- this is new
(fn [coeffects event]
(let [val (:local-store coeffects)
db (:db coeffects)]
{:db (assoc db :defaults val))}))
```
2016-08-15 12:54:23 +00:00
Problem solved? Well, no, but closer. We're assuming a `coeffects` function. How would it work?
### `coeffect` the function
2016-08-15 08:11:06 +00:00
The `coeffect` function is part of re-frame API.
2016-08-15 08:11:06 +00:00
It returns an Interceptor whose `:before` function loads a value into a `context's` `:coeffect` map.
2016-08-15 08:11:06 +00:00
It 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 08:11:06 +00:00
So in the case above, the `cofx-id` was `:local-store` and the additional value was "defaults-key".
2016-08-15 08:11:06 +00:00
### Other Example Uses of `coeffects`
2016-08-15 08:11:06 +00:00
Here's some other examples of its use:
- `(coeffects :random-int 10)`
- `(coeffects :guid)`
2016-08-15 08:11:06 +00:00
- `(coeffects :now)`
2016-08-15 08:11:06 +00:00
So, if I wanted to, I could create an event handler which has access to 3 coeffects:
```clj
(reg-event-fx
:some-id
[(coeffects :random-int 10) (coeffects :now) (coeffects :local-store "blah")] ;; 3
(fn [coeffects _]
... in here I can access coeffect's keys :now :local-store and :random-int))
```
2016-08-15 12:54:23 +00:00
Creating 3 coeffects is probably just showing off, and not generally necessary.
And we still have the final piece to put in place. How do we tell this `coeffect` function what to do when
it is given `:now` or `:random-int` ?
2016-08-15 08:11:06 +00:00
### Meet `reg-cofx`
2016-08-15 08:11:06 +00:00
This function allows you associate a`cofx-id` (like `:now` or `:local-store`) with a handler function.
2016-08-15 08:11:06 +00:00
The handler function registered for a `cofx-id` will be passed a `:coeffects` map, and it
is expected to return a modified map, presumably with an added key and value.
2016-08-15 08:11:06 +00:00
### Examples
2016-08-15 08:11:06 +00:00
You saw above the use of `:now`. He're is how a `cofx` handler could be registered:
```clj
(reg-cofx ;; using this new registration function
: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 to coeffects
```
2016-08-15 08:11:06 +00:00
And then there's this example:
```clj
(reg-cofx ;; new registration function
:local-store
(fn [coeffects lsk] ;; second parameter is the local store key
(assoc coeffects
:local-store
(js->clj (.getItem js/localStorage lsk))))
```
2016-08-15 08:11:06 +00:00
With these two registrations in place, I can now use `(coefect :now)` and
`(coeffect :local-store "blah")` in an effect handler's inteerceptor chain.
2016-08-15 12:54:23 +00:00
### The 4 Point Summary
Here is the overall picture, summarised, in note form ...
1. Event handlers should only source data from their arguments
2. So we have to "inject" required data into coeffect argument
3. So we use `(coeffects :key)` interceptor in registration
4. There has to be a coefx handler registered for that `:key`
XXX should "coeffect" function be called "inject" ... otherwise there just too many different coeffects
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`
and `reg-events-fx` add Interceptors to front of any chain during registration.
2016-08-15 12:54:23 +00:00
We found they inserted an Interceptor called `do-effects`. I can now reveal that
they also add `(coeffect :db)` at the front of each chain. (Last surprise, I promise)
2016-08-15 08:11:06 +00:00
Guess what that adds to coeffects?
2016-08-15 08:11:06 +00:00
### Testing
2016-08-15 12:54:23 +00:00
XXX