There are a couple of common problems with Re-frame subscriptions that I’d like to solve:
-
Because subscriptions are registered via keywords, the ClojureScript compiler does nothing to help check that we haven’t made a typo when subscribing.
-
We often need to use the same logic to access a value in the app DB in both a subscription and in events.
I’ve come up with the following structure. First we create the accessor function, which can be used by both the subscription and any events. (This of course implies that the namespace where we implement our events must refer to the namespace where we define our subscriptions.)
(defn widgets-by-type
[db t]
(->> (:widgets db)
(filter #(= t (:type %)))))
We then create the subscription based on the accessor.
(rf/reg-sub
::widgets-by-type
(fn [db [_ t]]
(widgets-by-type db t)))
Finally, we create a helper function for subscribing from within a component.
(defn <-widgets-by-type
[t]
@(rf/subscribe [::widgets-by-type t]))
Most components will use the last function to inject subscription values.
[:div (<-widgets-by-type :gadget)]
I’ve created a macro that allows us implement this pattern in one form.
(defsub widgets-by-type
(fn [db t]
(->> (:widgets db)
(filter #(= t (:type %))))))
Here is the macro definition.
(defmacro defsub
[sym & body]
(let [kw (keyword (str *ns*) (name sym))
impl (last body)
[_ [db & args] & impl-body] impl
]
`(do
(defn ~sym
[~db ~@args]
~@impl-body)
(rf/reg-sub
~kw
~@(butlast body)
(fn [~db [~(symbol "_") ~@args]]
(~sym ~db ~@args)))
(defn ~(symbol (str "<-" sym))
[~@args]
(deref (rf/subscribe [~kw ~@args]))))))
The macro supports chained subscriptions by just copying them over reg-sub
call. This makes defining subscriptions with defsub
almost identical to reg-sub
, with the exception that (a) we use a symbol instead of a keyword, and (b) we don’t need to destructure our event vector.
(defsub complex
(fn [[_ arg1] _]
[(rf/subscribe [:foo arg1])
(rf/subscribe [:bar arg1])])
(fn [[foo bar] arg1]
,,,))