-
-
Notifications
You must be signed in to change notification settings - Fork 716
Dynamic Subscriptions
This document is awaiting a rewrite - it is out of date.
The approach explained below will still work, but version 0.8.0 introduced new features/techniques meaning there's now a more modern way to do things.
Once rewritten, it will be moved within the repo
Dynamic Subscriptions have probably become unnecessary because of this proclamation https://github.com/Day8/re-frame/issues/218.
This theory of redundancy is effective from v0.8.0. But it is waiting to be confirmed by real world coding - so consider it provisional. Tell us if you continue to find a need for Dyn Subs.
Some explanation about how this proclamation impacts Dynamic Subscriptions can be found here in Corner Case #1 https://github.com/Day8/re-frame/issues/218#issuecomment-252470445
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.
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)))
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.
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:
-
caller
is called by someone further up the Reagent rendering chain -
caller
creates a subscription to:list-id
, returning a Reaction - A vector with the function
parent
and the subscription is returned up the rendering chain. - When Reagent comes to render the
parent
function, it dereferenceslist-id
, and renders thetodos-list
function (not shown here for brevity), passing down the list-id Reaction. We assign a React key to this Reagent component - Magic happens, the app is re-rendered.
- Some time passes, and the source for the
:list-id
subscription changes - The Reaction returned in step 2 changes to reflect the new value
- Reagent notices the changed value, and re-renders
caller
. - Because the key was based on
list-id
, andlist-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.
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.
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
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!
Deprecated Tutorials:
Reagent: