Skip to content

Dynamic Subscriptions

Daniel Compton edited this page Feb 9, 2016 · 17 revisions

N.B. This tutorial relies on knowledge and understanding of re-frame and Reagent. It's recommended to become comfortable with the other material in the wiki before reading this tutorial. This page is a first draft.

Introduction

Re-frame uses subscriptions to transfer data from the app-db to the view. You register subscriptions with register-sub by providing them a name and a handler function You subscribe to them in the view, by calling subscribe with the subscription name in a vector. You can also pass additional parameters to the handler function by passing them in the vector. Here's a short example:

(ns todoit.core
  (:require [re-frame.core :as r]))

(register-sub
  :todos                ;; usage:  (subscribe [:todos])
  (fn [db [_ list-id]]
      ;; Do something with list-id
      (reaction (vals (:todos @db)))))

(defn todo-app
  []
  (let [todos (subscribe [:todos "home"])] ;; Subscribe to the :todos subscription with list-id = "home"
    (fn []
      ;; Render @todos
      )))

These subscriptions work well for many use-cases, but sometimes you want your subscriptions to take dynamic parameters. Standard subscriptions won't work in this case without ugly workarounds. Dynamic subscriptions are the answer.

An example

Let’s say we work at todoit.computer and we just got $1M seed venture funding for our todo app. Our intial MVP with re-frame only had a single list, but the investors want to see multiple todo lists by the end of the quarter or they’re pulling the pin. You’re the lead developer, and the CEO is on your back about it. Let’s get started!

Our initial view looks like this, using a form-2 reagent component

(defn  todos-list
    [] 
    (let [list    (subscribe [:todos-list])]
        (fn []
        ... render the list)))

Option, the first

The first thing you might think of (we did too!) is something like this:

;; Don’t use this code!!!
(defn  todos-list
    [] 
    (let [list-id (subscribe [:list-id])
          list    (subscribe [:todos-list @list-id])]
        (fn []
        ... render the list)))

However this has a major flaw! list-id is dereferenced once when the view is initially rendered, and the value of list-id at that point in time is closed over in the subscription to :todos-list. When app-db changes, Reagent won't rerun this code, and you’ll be stuck with the first value that was dereferenced.

Option, the second

A dirty hack, is to do this:

(defn  parent 
    [list-id] 
    ^{:key @list-id}[todos-list list-id])

(defn caller []
  [parent (subscribe [:list-id])])

Let's step through how this works:

  1. caller is called by someone further up the Reagent rendering chain
  2. caller creates a subscription to :list-id, returning a Reaction
  3. A vector with the function parent and the subscription is returned up the rendering chain.
  4. When Reagent comes to render the parent function, it dereferences list-id, and renders the todos-list function (not shown here for brevity), passing down the list-id Reaction. We assign a React key to this Reagent component
  5. Magic happens, the app is re-rendered.
  6. Some time passes, and the source for the :list-id subscription changes
  7. The Reaction returned in step 2 changes to reflect the new value
  8. Reagent notices the changed value, and re-renders caller.
  9. Because the key was based on list-id, and list-id has changed, this component is invalidated. Reagent/React (?) destroys the old component and creates a new one based on the new key.

Whew! That was a pretty complex process to go through, and it's pretty dirty. If the CTO catches wind of it she won't be happy.

Option, the third

We could also rewrite our subscription so that it depended on list-id. This way, any change in list-id or our data source would cause a re-render. In the small, this might not seem like such a bad idea, but in the large, it could lead to a lot of very specific subscriptions, repeated code, and not being able to compose them more generally.

Our hero enters

Dynamic subscriptions allow you to create subscriptions that depend on Ratoms or Reactions (lets call them Signals). These subscriptions will be rerun when the Ratom or Reaction changes. You subscribe as usual with a vector like [:todos-list], and pass an additional vector of Signals. The Signals are dereferenced and passed to the handler-fn. Dynamic subscriptions need to pass a fn which takes app-db, the static vector, and the dereffed dynamic values.

Every time a dynamic value changes, handler-fn will be rerun. This is in contrast to standard subscriptions where handler-fn will only be run once, although the reaction that it produces will change over time.

(register-sub
  :todo-dynamic
  (fn todo-dynamic [_ _ [active-list]]
    (let [q (q/get-query active-list)]
      q)))

(register-sub
  :todos
  (fn todos [db _]
    (let [active-list (subscribe [:active-list])
          todos       (subscribe [:todo-dynamic] [active-list])]
      (make-reaction (fn todo-vals [] (update @todos :result #(vals (:list %))))))))

;; TODO: show view code here too

Success

You push the code and it goes through your continuous deployment chain into a Docker container and is deployed on our micro service platform on AWS. You step away from your standing desk and look at your Apple Watch. 4:50 pm. Just in time to grab some organic fruit and a craft beer. Good work!