Skip to content
Colin Yates edited this page Jul 24, 2015 · 60 revisions

1. Why Can't I access Subscriptions From Event Handlers?

Question

Those pesky re-frame "rules" say I can only use subscriptions in Components. But, in my event handlers, I need to use the same query. Why can't I use a subscription there too?

I am the Che Guevara of the Reagent world, and I will not be oppressed by their filthy capitalist, pig dog rules!!!

Answer

You should think about a subscription handler as having two parts:

  1. A query function db -> val.
  2. a reaction wrapping

The reaction wrapping delivers "a stream" of updates over time. The means the query will be rerun whenever "app-db" changes, and that's perfect for Components which, in response, might need to rerender

But event handlers don't need that. They need to do a one query, which yields one result, all based off the db param supplied. They don't need a stream of query results.

So, if you find yourself needing to query db in your event handlers, and wishing you could use a subscription:

  • factor out the reusable query into a function
  • within your subscription, use that function, and wrap it in a reaction (to get a stream of values)
  • within your event handlers, call the function directly (to get a single value).

Sketch:

(defn my-query
   [db v]
   .....)   ;; return some interesting value based on db

;; a subscription handler
;; needs to produce a "stream" of changes, based on my-query
(register-sub 
  :some-id
  (fn [app-db v]
    (reaction (my-query @app-db v)))) ;; use my-query with @app-dp, in reaction

;; an event handler
;; needs to perform the query once, to obtain a value.
(register-handler
  :h-id
  (fn [db v] 
    (let [calc   (my-query db v)]        ;; use my-query to get a one off value
       .... use calc)))

So now my-query is available for use by event handlers, free of the reaction wrapping.

And, yes, come the revolution, I'm sure we'll be the first ones against the wall. :-)

2. Can I Subscribe in A Subscription Handler?

Question

I'd like to call one subscription in another subscription handler. Is that okay? If so, how should I do it?

Answer

Yes, it is fine to do that. Here's an example:

(register-sub
  :data
  (fn [db _] (reaction (:data @db))))

(register-sub
  :sort-order
  (fn [db _] (reaction (:sort-order @db))))

(register-sub
  :filter
  (fn [db _] (reaction (:filter @db))))

(register-sub
  :table-data
  (fn [db _] 
    (let [data          (subscribe [:data])          ;; <--- subscribe used here
          type-filter   (subscribe [:filter])        ;; <--- and here
          sort-order    (subscribe [:sort-order])]   ;; <--- and here

    ;; the final returned reaction uses returns from the subscribe calls above
    ;; remember to deref the returned values
    (reaction (sort-by @sort-order (filter #(= (:type %) @filter) @data)))))

Note: in the above, the subscribe calls are not themselves inside a reaction. Rather the return of these calls is used in the final reaction. The following version is the wrong way to do it::

(register-sub
  :table-data
  (fn [db _] 
    (reaction              ;; <---  reaction wraps calls to subscribe - IS WRONG
      (let [data          (subscribe [:data])                
            type-filter   (subscribe [:filter])              
            sort-order    (subscribe [:sort-order])]         
      (sort-by @sort-order (filter #(= (:type %) @filter) @data)))))

3. Why Can't I Call dispatch-sync In An Event Handler?

Question

In an event handler, I'm allowed to dispatch further events. But I'm not allowed to use dispatch-sync. Why? Aren't they pretty much the same?

Answer

As a general rule, you should always use dispatch. Only use dispatch-sync if you specifically need it but, as this FAQ explains, never try to in an event handler.

dispatch and dispatch-sync are identical in intent, but they differ in terms of when the event's handler is run:

  • dispatch queues the event for handling "later"
  • dispatch-sync runs the associated event handler RIGHT NOW.

This "later" vs "right now" difference is the key.

If we are currently halfway through running one event handler, and we:

  1. dispatch an event - it will be handled sometime AFTER the current handler completes.
  2. dispatch-sync an event - it will be handled immediately, before the current handler completes.

To illustrate, assume we have these two simple event handlers:

(register-handler
  :a
  (fn [db _]
    (assoc db :a 100)))

(register-handler
  :b
  (fn [db _]
    (dispatch-sync [:a])      ;; <-- dispatch-sync used here
    (assoc db :b 5)))

If we were to: (dispatch [:b]) and then, afterwards, inspect app-db we'd see:

  • :b with a value of 5
  • no change :a - surprisingly it doesn't have the value 100

It is as if (dispatch-sync [:a]) never happened. Its modification to :a is lost.

Here's why. Because dispatch-sync is used, the process is:

  1. event handler for [:a] called with db snapshot
  2. event handler for [:b] called, with db snapshot
  3. event handler for [:b] returns modified db which is put into app-db
  4. event handler for [:a] returns modified db which is put into app-db

Step 4 overwrites step 3, which means that step 2 is lost.

re-frame detects nested handler calls and will produce a warning if it occurs. This FAQ entry is here mostly to explain why you got that error.

3. Can re-frame use my logging functions?

Question

I use logging technique X, how do I make re-frame use my method?

Answer

re-frame makes use of the logging functions: warn, log, error, group and groupEnd.

By default, these functions map directly to the js/console implementations, but you can override that by providing your own set or subset of these functions.

Use re-frame.core/set-loggers! like this:

(defn my-warn
   [text]       ;; text is a string
   .... I'll warn about 'text' in here)

(defn my-log
   [text]
   ....)

(set-loggers!  {:warn  my-warn   :log  my-log ...})

4. How can I denormalise data within a re-frame application?

I have a pretty normalised database and want to stitch it together when I display it. For example, I have many wibblies and I want to display them in a table, but each wibble has a wobble. How should I do that?

One way is to use a form-2 component which accepts an id and subscribes to a denormalising subscription based on that id:

  (defn my-cmp [id]
    (let [denormalised-state (subscribe [:denormaliser id])]
      (fn [id]
        [:div (:some-denormalised-state @denormalised-state)])))

See here for more info.

N. Can I Add An FAQ Entry?

Question

I'd like to add an FAQ question and answer. Can I do that?

Answer

Yes, please put one here if you think it useful! Then open an issue to get it reviewed. Many Thanks!!