223 lines
7.5 KiB
Markdown
223 lines
7.5 KiB
Markdown
## Introduction
|
|
|
|
This tutorial explains `coeffects`.
|
|
|
|
It explains what they are, how they help, how they can be "injected", and how
|
|
to manage them in tests.
|
|
|
|
## Table Of Contexts
|
|
|
|
- [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)
|
|
* [The 4 Point Summary](#the-4-point-summary)
|
|
* [Secret Interceptors](#secret-interceptors)
|
|
* [Testing](#testing)
|
|
|
|
## Coeffects
|
|
|
|
### 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`,
|
|
which delivers 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,
|
|
or the current datetime. It might even need access to a
|
|
DataScript connection.
|
|
|
|
|
|
### An Example
|
|
|
|
This handler obtains data directly from LocalStore
|
|
```clj
|
|
(reg-event-db
|
|
:load-defaults
|
|
(fn [coeffects _]
|
|
(let [val (js->clj (.getItem js/localStorage "defaults-key"))] ;; <-- Problem
|
|
(assoc db :defaults val))))
|
|
```
|
|
|
|
Because it has accessed LocalStore, this event handler is not
|
|
pure, and impure functions cause well-documented paper cuts.
|
|
|
|
### How We Want It
|
|
|
|
Our goal in this tutorial is to rewrite this event handler so
|
|
that data _only_ comes from the arguments.
|
|
|
|
Our first change is to start using `reg-event-fx` (instead of
|
|
`reg-event-db`).
|
|
|
|
Then we'll seek to have ALL the necessary extra data available in the
|
|
first argument, typically called `coeffects`.
|
|
|
|
Previous tutorials have show us that we can obtain `:db` from
|
|
`coeffects`. Well, not we want it to contain other useful data too.
|
|
```clj
|
|
(reg-event-fx ;; note: -fx
|
|
: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
|
|
```
|
|
|
|
If we can find a way to achieve this, then we are back to
|
|
writing pure event handlers.
|
|
|
|
But what must we do to data into cofx? How do we organise for it
|
|
to contain a `:local-store` key, with the right value?
|
|
|
|
### How Are Coeffect Babies Made?
|
|
|
|
Well, when two coeffects love each other very much ... no, stop ... this
|
|
is a G-rated framework. Instead ...
|
|
|
|
Each time an event handler is executed, a brand new `context` is created, and within that
|
|
`context` is a brand new `:coeffect` map, which is initially totally empty.
|
|
|
|
That pristine `context` value (containing a pristine `:coeffect` map) is then threaded
|
|
through a chain of Interceptors before it is finally handled to our event handler
|
|
which will be sitting on the end of 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` gets made. This is where
|
|
new keys are added to `:coeffect`, so that later our handler magically finds the
|
|
right data in its parameter.
|
|
|
|
### So, Next Step
|
|
|
|
If Interceptors put data in `:coeffect`, then we'd better put the right ones on
|
|
our handler when we register it.
|
|
|
|
This handler is the same as before, except for one addition:
|
|
```clj
|
|
(reg-event-fx
|
|
: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))}))
|
|
```
|
|
|
|
So we've added one Interceptor. It will inject the right value into `context's` `:coeffeects`
|
|
and that `:coeffects` ends up being the first parameter to our handler.
|
|
|
|
|
|
### `inject-cofx`
|
|
|
|
`inject-cofx` is part of re-frame API.
|
|
|
|
It is a function which returns an Interceptor whose `:before` function loads
|
|
a value into a `context's` `:coeffect` 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
|
|
was "defaults-key" which was presumable the place to look in LocalStore
|
|
|
|
### More `inject-cofx`
|
|
|
|
Here's some other examples of its use:
|
|
|
|
- `(inject-cofx :random-int 10)`
|
|
- `(inject-cofx :guid)`
|
|
- `(inject-cofx :now)`
|
|
|
|
So, if I wanted to, I could create an event handler which has access to 3 coeffects:
|
|
```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))
|
|
```
|
|
|
|
Creating 3 coeffects for the one handler is probably just showing off, and not generally necessary.
|
|
|
|
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.
|
|
|
|
### Meet `reg-cofx`
|
|
|
|
This function allows you associate a`cofx-id` (like `:now` or `:local-store`) with a
|
|
handler function that you supply.
|
|
|
|
The handler function you register for a given `cofx-id` will be passed two arguments:
|
|
- a `:coeffects` map, and
|
|
- the optional value supplied
|
|
and it is expected to return a modified `:coeffects` map, presumably with an
|
|
added key and value.
|
|
|
|
### Examples
|
|
|
|
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 ;; using this new registration function
|
|
:now ;; what cofx-id are we registering
|
|
(fn [cofx _] ;; second parameter not used in this case
|
|
(assoc cofx :now (js.Date.)))) ;; add :now key, with value
|
|
```
|
|
|
|
And then there's this example:
|
|
```clj
|
|
(reg-cofx ;; new registration function
|
|
:local-store
|
|
(fn [coeffects local-store-key]
|
|
(assoc coeffects
|
|
:local-store
|
|
(js->clj (.getItem js/localStorage local-store-key))))
|
|
```
|
|
|
|
|
|
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.
|
|
|
|
### The 4 Point Summary
|
|
|
|
Here's 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 `(inject-cofx :key)` interceptor in registration of the event handler
|
|
4. There has to be a coefx handler registered for that `:key` (using `reg-cofx`)
|
|
|
|
|
|
### 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`. I can now reveal that
|
|
they also add `(inject-cofx :db)` at the front of each chain. (Last surprise, I promise)
|
|
|
|
Guess what that adds to the `:coeffects` of every event handler?
|
|
|
|
### Testing
|
|
|
|
During testing, you may want to stub out certain
|
|
|
|
|
|
|