Skip to content

Why didn't my component update?

Reagent components should "watch" their input arguments and re-render whenever they change value. But sometimes this fails to happen, for non-obvious reasons.

Answer

Several causes are explained by the reagent project: FAQ: Component Not Rerendering.

Here are some additional problems we've faced in re-frame apps:

You dereferenced a subscription outside the render-fn

Consider this reagent component. When the value for :some-data changes within app-db, will the component update?

(reg-sub :my-data (fn [db] (get db :my-data)))

(defn my-component []
  (let [current-data @(rf/subscribe [:my-data])]
    (str current-data)))

Yes, this works as you'd expect. But here's another component - it appears deceptively similar, but technically it is now a form-2 component.

(defn my-component []
  (let [current-data @(rf/subscribe [:my-data])]
    (fn my-render-fn [] (str current-data))))

Here, current-data really isn't current! When this component mounts, you'll see the correct value within app-db. But as that value changes, what you see will remain the same. Why?

The key is to understand what reagent does with each function you pass it:

in the case of Form-2, the outer function is called once and once only
Appendix A, section 5

That means reagent only calls my-component once. That call dereferences your subscription and then returns my-render-fn, which reagent calls "many, many times thereafter." But, my-render-fn uses current-data, an immutable binding which will always refer back to that first dereferenced value.

Put simply, the solution is to dereference subscriptions within the actual render function, not this constructor-like outer function. And, if it feels helpful, try using * to suggest that your symbol is bound to a derefable:

(defn my-component []
  (let [current-data* (rf/subscribe [:my-data])]
    (fn my-render-fn [] (str @current-data*))))

Now, my-render-fn can get called "many, many times," and new values for current-data* will flow in, even as the lexical scope stays the same. It doesn't hurt to structure your original form-1 component in the same way, dereferencing subscriptions in the body form rather than in the let bindings.

Updating from a form-1 to a form-2 (or form-3) is an ordinary step in our development process - for instance, to declare some component-local state. But without a keen eye for reactivity, it's easy to break your components.

You used defmulti

A subtle but severe problem can occur if you implement a form-2 component using defmulti/defmethod:

(defn which-fruit [k] k)

(defmulti fruit-view which-fruit)

(defmethod fruit-view :apple [_]
  [:li "🍎 I'm an apple!"])

(defmethod fruit-view :banana [_]
  (fn render-banana []
    [:li "🍌 I'm a banana!"]))

(def fruit (r/atom :apple))

(defn next-fruit! []
  (swap! fruit {:apple  :banana
                :banana :apple}))

(defn fruit-switcher []
  [:div
   [:button {:on-click next-fruit!} "Click me!"]
   [:div "Showing " @fruit ":"]
   [fruit-view @fruit]])

On the first render, reagent invokes fruit-view. Clojure runs your dispatch function which-fruit, looks up the method for :apple, and calls it, returning a hiccup. Since fruit-view returns a hiccup, reagent considers it to be a form-1 component. It renders that hiccup directly. So far, so good.

Then, you click the button. The value of fruit changes, causing reagent to invoke fruit-view again. This time, we get the :banana method, which returns render-banana (not a hiccup). Here's where the problem begins: since our fruit-view now returns a function, reagent now considers it to be form-2 !

As explained above, reagent calls this new form-2 component function "once and once only". Subsequently, every time fruit-view re-renders, reagent will not call your multimethod, and no dispatch will happen. Instead, reagent has stored render-banana in its cache, and it will always call render-banana, showing "I'm a banana!" forever.

In effect, we've created a nasty dynamism where our component works perfectly well... until it doesn't. It could work for hours, until the user clicks that one button. Or it could work for years, if your codebase only contains form-1 methods, until someone decides to add a form-2 method.

Unfortunately, we haven't found a perfect way to use multimethods with reagent. One workaround is to use defmulti to implement only form-1 components. For instance, you could define your components using regular defn, and use defmulti only as a wrapper to dispatch them:

(defn fruit-item-apple [_]
  (fn [_] [:li "🍎 I'm an apple!"]))

(defmethod fruit-item :apple [props] [fruit-item-apple])

(defn fruit-item-banana [_]
  (fn [_] [:li "🍌 I'm a banana!"]))

(defmethod fruit-item :banana [props] [fruit-item-banana props])

Or, you could extract your form-2 code into a single wrapper, and call your multimethod from there, making sure all your methods are form-1:

(defmethod fruit-item :apple [_]
  [:li "🍎 I'm an apple!"])

(defmethod fruit-item :banana [_]
  [:li "🍌 I'm a banana!"])

(defn fruit-view 
  "We need this wrapper component to ensure reactivity.
  For details, refer to this article: 
  https://day8.github.io/re-frame/FAQs/why-didnt-my-component-update"
  [k]
  (let [] ;; Do your form-2 stuff here.
    (fn []
      [fruit-item k])))

If you truly need a polymorphic form-2 component without caveats, consider not using defmulti. Though multimethods are elegant, our workarounds can be clumsy.

Consider using case or cond, instead. This can work as idiomatic Clojure, providing explicit and foolproof dispatch without depending on incidental indirections like fruit-view above:

(defn fruit-item-apple []
  (fn [] ;; No problem using form-2
    [:li "🍎 I'm an apple!"]))

(defn fruit-item-banana []
  [:li "🍌 I'm a banana!"])

(defn fruit-view [k]
  (case k
    :apple  [fruit-item-apple]
    :banana [fruit-item-banana]))