mirror of
https://github.com/status-im/re-frame.git
synced 2025-02-23 15:28:09 +00:00
The function coeffect
is renamed to inject-cofx
.
When writing the docs I found that sentences had too many mentions of `coeffects` with different meanings, which was confusing. With this change, I remove one possible meaning of `coeffect` and, in the process I end up with a clearer name anyway.
This commit is contained in:
parent
1efcea2cf2
commit
4f2f772afd
@ -2,8 +2,8 @@
|
||||
|
||||
This tutorial explains `coeffects`.
|
||||
|
||||
It explains what they are, how they are created, how they help, and how
|
||||
to manage them in tests. There's also an adults-only moment.
|
||||
It explains what they are, how they help, how they can be "injected", and how
|
||||
to manage them in tests.
|
||||
|
||||
## Table Of Contexts
|
||||
|
||||
@ -27,16 +27,16 @@ to manage them in tests. There's also an adults-only moment.
|
||||
|
||||
### What Are They?
|
||||
|
||||
`coeffects` are the input resources that an event handler requires
|
||||
to perform its computation. By "resources" I mean "data".
|
||||
`coeffects` are the data resources that an event handler needs
|
||||
to perform its computation.
|
||||
|
||||
Because the majority of event handlers require only `db` and
|
||||
`event`, there's a registration function, called `reg-event-db`,
|
||||
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 it easy.
|
||||
handler, making this common case easy to program.
|
||||
|
||||
But sometimes an event handler needs other inputs
|
||||
to perform their computation. Things like a random number, or a GUID,
|
||||
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.
|
||||
|
||||
@ -45,7 +45,7 @@ DataScript connection.
|
||||
|
||||
This handler obtains data directly from LocalStore
|
||||
```clj
|
||||
(reg-event-fx
|
||||
(reg-event-db
|
||||
:load-defaults
|
||||
(fn [coeffects _]
|
||||
(let [val (js->clj (.getItem js/localStorage "defaults-key"))] ;; <-- Problem
|
||||
@ -53,148 +53,170 @@ This handler obtains data directly from LocalStore
|
||||
```
|
||||
|
||||
Because it has accessed LocalStore, this event handler is not
|
||||
pure, which will trigger well documented paper cuts.
|
||||
pure, and impure functions cause well-documented paper cuts.
|
||||
|
||||
### Let's Fix It
|
||||
### How We Want It
|
||||
|
||||
Here's how __we'd like to rewrite that handler__. Data should
|
||||
only come from the `coeffects` argument:
|
||||
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 ;; using -fx registration
|
||||
(reg-event-fx ;; note: -fx
|
||||
:load-defaults
|
||||
(fn [coeffects event] ;; 1st argument is coeffects
|
||||
(let [val (:local-store coeffects) ;; <-- get value from argument
|
||||
db (:db coeffects)]
|
||||
(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
|
||||
```
|
||||
|
||||
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`.
|
||||
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 ...
|
||||
|
||||
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.
|
||||
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 is then threaded through a chain of Interceptors, and sitting
|
||||
on the end of chain is the event handler, wrapped up as the final interceptor. We know
|
||||
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.
|
||||
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
|
||||
|
||||
Armed with that mindset, let's add an interceptor to the registration, which
|
||||
puts the right localstore value into coeffect.
|
||||
If Interceptors put data in `:coeffect`, then we'd better put the right ones on
|
||||
our handler when we register it.
|
||||
|
||||
Here's a sketch:
|
||||
This handler is the same as before, except for one addition:
|
||||
```clj
|
||||
(reg-event-fx
|
||||
:load-defaults
|
||||
[ (coeffect :local-store "defaults-key") ] ;; <-- this is new
|
||||
(fn [coeffects event]
|
||||
(let [val (:local-store coeffects)
|
||||
db (:db coeffects)]
|
||||
[ (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))}))
|
||||
```
|
||||
|
||||
Problem solved? Well, no, but closer. We're assuming a `coeffects` function. How would it work?
|
||||
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.
|
||||
|
||||
|
||||
### `coeffect` the function
|
||||
### `inject-cofx`
|
||||
|
||||
The `coeffect` function is part of re-frame API.
|
||||
`inject-cofx` is part of re-frame API.
|
||||
|
||||
It returns an Interceptor whose `:before` function loads a value into a `context's` `:coeffect` map.
|
||||
It is a function which returns an Interceptor whose `:before` function loads
|
||||
a value into a `context's` `:coeffect` map.
|
||||
|
||||
|
||||
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.
|
||||
`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".
|
||||
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
|
||||
|
||||
### Other Example Uses of `coeffects`
|
||||
### More `inject-cofx`
|
||||
|
||||
Here's some other examples of its use:
|
||||
|
||||
- `(coeffects :random-int 10)`
|
||||
- `(coeffects :guid)`
|
||||
- `(coeffects :now)`
|
||||
- `(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
|
||||
[(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))
|
||||
[(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 is probably just showing off, and not generally necessary.
|
||||
Creating 3 coeffects for the one handler 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` ?
|
||||
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.
|
||||
This function allows you associate a`cofx-id` (like `:now` or `:local-store`) with a
|
||||
handler function that you supply.
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
You saw above the use of `:now`. He're is how a `cofx` handler could be registered:
|
||||
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 [coeffects _] ;; second parameter not used in this case
|
||||
(assoc coeffects :now (js.Date.)))) ;; add :now key, with value to coeffects
|
||||
(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 lsk] ;; second parameter is the local store key
|
||||
(fn [coeffects local-store-key]
|
||||
(assoc coeffects
|
||||
:local-store
|
||||
(js->clj (.getItem js/localStorage lsk))))
|
||||
(js->clj (.getItem js/localStorage local-store-key))))
|
||||
```
|
||||
|
||||
|
||||
With these two registrations in place, I can now use `(coefect :now)` and
|
||||
`(coeffect :local-store "blah")` in an effect handler's inteerceptor chain.
|
||||
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 is the overall picture, summarised, in note form ...
|
||||
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 `(coeffects :key)` interceptor in registration
|
||||
4. There has to be a coefx handler registered for that `:key`
|
||||
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`)
|
||||
|
||||
XXX should "coeffect" function be called "inject" ... otherwise there just too many different coeffects
|
||||
|
||||
|
||||
### 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-effects`. I can now reveal that
|
||||
they also add `(coeffect :db)` at the front of each chain. (Last surprise, I promise)
|
||||
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 coeffects?
|
||||
Guess what that adds to the `:coeffects` of every event handler?
|
||||
|
||||
### Testing
|
||||
|
||||
XXX
|
||||
During testing, you may want to stub out certain
|
||||
|
||||
|
||||
|
||||
|
@ -12,17 +12,18 @@
|
||||
(assert (re-frame.registrar/kinds kind))
|
||||
(def register (partial register-handler kind))
|
||||
|
||||
|
||||
;; -- Interceptor -------------------------------------------------------------
|
||||
|
||||
(defn coeffect
|
||||
"An interceptor which adds to a `context's` `:coeffects`.
|
||||
(defn inject-cofx
|
||||
"Returns an interceptor which adds to a `context's` `:coeffects`.
|
||||
|
||||
`coeffects` are the input resources required by an event handler
|
||||
to perform its job. The two most obvious ones are `db` and `event`.
|
||||
But sometimes a handler might need other resources.
|
||||
|
||||
Perhaps a handler needs a random number or a GUID or the current datetime.
|
||||
Perhaps it needs access to an in-memory DataScript database.
|
||||
Perhaps it needs access to the connection to a DataScript database.
|
||||
|
||||
If the handler directly access these resources, it stops being as
|
||||
pure. It immedaitely becomes harder to test, etc.
|
||||
@ -30,29 +31,26 @@
|
||||
So the necessary resources are \"injected\" into the `coeffect` (map)
|
||||
given the handler.
|
||||
|
||||
Given one or more `ids`, this function will iterately lookup the
|
||||
registered coeffect handlers (via `reg-cofx`) and call each of them
|
||||
giving the current `:coeffect` as an argument, and expecting a
|
||||
modified coeffect to be returned.
|
||||
Given an `id`, and an optional value, lookup the registered coeffect
|
||||
handler (previously registered via `reg-cofx`) and it with two arguments:
|
||||
the current value of `:coeffect` and, optionally, the value. The registered handler
|
||||
is expected to return a modified coeffect.
|
||||
"
|
||||
;; Why? We want our handlers to be pure. If a handler calls `js/Date.` then
|
||||
;; it stops being as pure. It is harder to test.
|
||||
;;
|
||||
;; And what if a handler needs a random number? Or a GUID? These kinds of input resources are
|
||||
;; termed `coeffects` (sometimes called side-causes).
|
||||
[& ids]
|
||||
([id]
|
||||
(->interceptor
|
||||
:name :coeffects
|
||||
:before (fn coeffects-before
|
||||
[context]
|
||||
(let [orig-coeffect (:coeffects context)
|
||||
run-id (fn [coeffect id]
|
||||
((get-handler kind id) coeffect))
|
||||
new-coeffect (reduce run-id orig-coeffect ids)]
|
||||
(assoc context :coeffects new-coeffect)))))
|
||||
(update context :coeffects (get-handler kind id)))))
|
||||
([id value]
|
||||
(->interceptor
|
||||
:name :coeffects
|
||||
:before (fn coeffects-before
|
||||
[context]
|
||||
(update context :coeffects (get-handler kind id) value)))))
|
||||
|
||||
|
||||
;; -- Standard Builtin CoEffects Handlers --------------------------------------------------------
|
||||
;; -- Builtin CoEffects Handlers ---------------------------------------------
|
||||
|
||||
;; :db
|
||||
;;
|
||||
@ -64,13 +62,17 @@
|
||||
(assoc coeffects :db @app-db)))
|
||||
|
||||
|
||||
;; this interceptor is so commonly used that we reify it
|
||||
(def add-db (coeffect :db))
|
||||
;; Because this interceptor is used so much, we reify it
|
||||
(def inject-db (inject-cofx :db))
|
||||
|
||||
(register
|
||||
:local-store
|
||||
(fn local-store-handler
|
||||
[coeffects k]
|
||||
()XXXX
|
||||
(assoc coeffects :db @app-db)))
|
||||
|
||||
;; XXX what about a coeffect which reads LocalStore. Use in todomvc example.
|
||||
|
||||
;; -- Example ------------------------------------------------------------------------------------
|
||||
;; -- Further Example --------------------------------------------------------
|
||||
|
||||
;; An example coeffect handler, which adds the current datetime under
|
||||
;; the `:now` key.
|
||||
|
@ -55,6 +55,7 @@
|
||||
|
||||
;; -- coeffects
|
||||
(def reg-cofx cofx/register)
|
||||
(def inject-cofx cofx/inject-cofx)
|
||||
(def clear-cofx (partial registrar/clear-handlers cofx/kind))
|
||||
|
||||
|
||||
@ -73,21 +74,21 @@
|
||||
([id db-handler]
|
||||
(reg-event-db id nil db-handler))
|
||||
([id interceptors db-handler]
|
||||
(events/register id [cofx/add-db fx/do-effects interceptors (db-handler->interceptor db-handler)])))
|
||||
(events/register id [cofx/inject-db fx/do-fx interceptors (db-handler->interceptor db-handler)])))
|
||||
|
||||
|
||||
(defn reg-event-fx
|
||||
([id fx-handler]
|
||||
(reg-event-fx id nil fx-handler))
|
||||
([id interceptors fx-handler]
|
||||
(events/register id [cofx/add-db fx/do-effects interceptors (fx-handler->interceptor fx-handler)])))
|
||||
(events/register id [cofx/inject-db fx/do-fx interceptors (fx-handler->interceptor fx-handler)])))
|
||||
|
||||
|
||||
(defn reg-event-ctx
|
||||
([id handler]
|
||||
(reg-event-ctx id nil handler))
|
||||
([id interceptors handler]
|
||||
(events/register id [cofx/add-db fx/do-effects interceptors (ctx-handler->interceptor handler)])))
|
||||
(events/register id [cofx/inject-db fx/do-fx interceptors (ctx-handler->interceptor handler)])))
|
||||
|
||||
|
||||
;; -- Logging -----
|
||||
|
Loading…
x
Reference in New Issue
Block a user