Skip to content

Latest commit

 

History

History
139 lines (96 loc) · 4.94 KB

UseASubscriptionInAnEventHandler.md

File metadata and controls

139 lines (96 loc) · 4.94 KB

Question

How do I access the value of a subscription from within an event handler?

The Root Problem

Subscriptions are stateful. That said, they offer a 90% solution where you don't have to worry about their state. But this comes with a caveat: the only safe place to call subscribe is within a reagent component function.

!!! Note See Flows - Reactive Context for an in-depth explanation.

DOM event handlers

Inner functions, such as DOM event handlers, don't count. Consider this component:

(defn my-btn []
  [:button {:on-click #(subscribe [:some (gensym)])}])

Our :on-click function isn't actually called when the component renders. Instead, we've given the function to the browser, expecting it to get called later. The problem is, reagent and re-frame have no way to safely manage your subscription at that time. The result is a memory leak. If the browser calls your :on-click a thousand times, re-frame will "create" a thousand unique subscriptions, and there's no code in place to "dispose" them later.

Re-frame event handlers

Re-frame event handlers don't count either. Re-frame calls your event handlers within its own loop, which runs in a totally separate context from reagent's render loop.

(re-frame.core/reg-event-db
  :event-id 
  (fn [db v]
    (let [sub-val  @(subscribe [:something])]   ;; <--- Possible memory leak
       ....)))

Furthermore, this is a conceptual issue. Your subscription handler may be pure, but subscribe always has a side-effect. Calling subscribe inside an event handler goes against re-frame's design, which is based on handlers being pure functions.

Incidental safety

Calling subscribe outside a component is somewhat safe, as long as you've also called it inside a component. The outside one has no way to dispose, but the inside one might dispose later.

Of course, that requires your component to be around while your other code runs. If that component unmounts and never comes back, then you're on your own again.

This isn't a real solution, it's just incidental safety.

Solutions

Restructure your app

Sometimes it's enough to factor out your calculations, so they can be shared between subscription and event handlers.

Don't call subscribe in your event handler:

(reg-sub :areas (fn [db] (map (fn [r] (* Math/PI r r)) (:circles db))))

(reg-event-fx 
  :store-areas
  (fn [{:keys [db]} _]
    {:local-store @(subscribe [:areas])}))

Do factor out calculation helpers:

(defn circle-area [r] (* Math/PI r r))

(def get-areas (comp circle-area :circles))

(reg-sub areas (fn [db _] (get-areas db)))

(reg-event-fx 
  :store-areas
  (fn [{:keys [db]} _]
    {:local-store (get-areas db)}))

Don't call subscribe in a callback:

[:input {:type "button" 
         :value "Click me!"
         :on-click #(doto @(subscribe [:circles]) circle-effect!)}]

Do dispatch an event in a callback:

(rf/reg-fx :circle-effect circle-effect!)

(re-frame.core/reg-event-fx
  :clicked-button
  (fn [{{:keys [circles]} :db :as coeffects} event-v]
    {:circle-effect circles}))

[:input {:type "button" 
        :value "Click me!"
        :on-click #(dispatch [:clicked-button %])}]

Flows

Instead of a subscription, consider using re-frame.flow. A flow gives you a derived value that's accessible anywhere, any time.

Experimental Subscriptions

re-frame.alpha provides subscriptions with custom lifecycles. You can pass a :re-frame/lifecycle key to create subscriptions with various performance profiles and levels of memory safety. By default, re-frame.alpha/sub creates a subscription which understands reactive context enough to guarantee memory safety. The tradeoff is that calling sub outside a component won't be cached at all. It will recalculate every time you call sub, unless you've already created the subscription inside a mounted component.

re-frame-utils

The 3rd party library re-frame-utils provides an inject coeffect. This allows you to access a subscription's value within a re-frame event handler.

This way, you declare an interceptor that resolves your subscription. Then, your event handler function can destructure the value from the coeffects:

(re-frame.core/reg-event-fx         ;; handler must access coeffects, so use -fx
  :event-id 
  (vimsical.re-frame.cofx.inject/inject [:query-id :param])  ;; <-- interceptor will inject subscription value into coeffects
  (fn [coeffects event]
    (let [sub-val (:something coeffects)]  ;; obtain subscription value 
       ....)))