re-frame/docs/coeffects.md

6.2 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. Perhaps it might need access to an in-memory DataScript database.

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 has stopped being 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. We're still sketching here. What's that coeffects function returning and how does it know to load from LocalStore.

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

Probably excessive. But we're still missing the final piece. How do we register a coeffect handler so that this function coeffect knows what value you want?

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.

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.

If you remember, they insert an Interceptor called do-effects, which ultimately actions effects, in its :after function.

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

re=-frame has very few XXX

This plugable approach,