re-frame/docs/coeffects.md

6.6 KiB

Introduction

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.

Table Of Contexts

Coeffects

What Are They?

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 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, or the current datetime. It might even need access to a DataScript connection.

An Example

This handler obtains data directly from LocalStore

(reg-event-fx
   :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, which will trigger well documented paper cuts.

Let's Fix It

Here's how we'd like to rewrite that handler. Data should only come from the coeffects argument:

(reg-event-fx                     ;; using -fx registration
   :load-defaults
   (fn [coeffects event]         ;; 1st argument is coeffects
     (let [val (:local-store coeffects)  ;; <-- get value from argument
           db   (:db coeffects)]  
       {: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.

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.

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
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, Next Step

Armed with that mindset, let's add an interceptor to the registration, which puts the right localstore value into coeffect.

Here's a sketch:

(reg-event-fx                     
   :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))})) 

Problem solved? Well, no, but closer. We're assuming a coeffects function. How would it work?

coeffect the function

The coeffect function is part of re-frame API.

It 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.

So in the case above, the cofx-id was :local-store and the additional value was "defaults-key".

Other Example Uses of coeffects

Here's some other examples of its use:

  • (coeffects :random-int 10)
  • (coeffects :guid)
  • (coeffects :now)

So, if I wanted to, I could create an event handler which has access to 3 coeffects:

(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)) 

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 ?

Meet reg-cofx

This function allows you associate acofx-id (like :now or :local-store) with a handler function.

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.

Examples

You saw above the use of :now. He're is how a cofx handler could be registered:

(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

And then there's this example:

(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))))

With these two registrations in place, I can now use (coefect :now) and (coeffect :local-store "blah") in an effect handler's inteerceptor chain.

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

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)

Guess what that adds to coeffects?

Testing

XXX