Skip to content

Commit 9b2dee6

Browse files
author
kk
committed
[docs] Explain some component update issues
1 parent 016972a commit 9b2dee6

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#
2+
3+
## Why didn't my component update?
4+
5+
Reagent components should "watch" their input arguments and re-render whenever they change value.
6+
But sometimes this fails to happen, for non-obvious reasons.
7+
8+
## Answer
9+
10+
Several causes are explained by the reagent project:
11+
[FAQ: Component Not Rerendering](https://github.com/reagent-project/reagent/blob/master/doc/FAQ/ComponentNotRerendering.md).
12+
13+
Here are some additional problems we've faced in re-frame apps:
14+
15+
### You dereferenced a subscription outside the render-fn
16+
17+
Consider this reagent component. When the value for `:some-data` changes within app-db,
18+
will the component update?
19+
20+
```clojure
21+
(reg-sub :my-data (fn [db] (get db :my-data)))
22+
23+
(defn my-component []
24+
(let [current-data @(rf/subscribe [:my-data])]
25+
(str current-data)))
26+
```
27+
28+
Yes, this works as you'd expect. But here's another component -
29+
it appears deceptively similar, but technically it is now a
30+
[form-2 component](https://github.com/reagent-project/reagent/blob/master/doc/CreatingReagentComponents.md#form-2--a-function-returning-a-function).
31+
32+
```clojure
33+
(defn my-component []
34+
(let [current-data @(rf/subscribe [:my-data])]
35+
(fn my-render-fn [] (str current-data))))
36+
```
37+
38+
Here, `current-data` really _isn't_ current!
39+
When this component mounts, you'll see the correct value within app-db.
40+
But as that value changes, what you see will remain the same. Why?
41+
42+
The key is to understand what reagent does with each function you pass it:
43+
44+
> in the case of `Form-2`, the outer function is called once and once only
45+
> [Appendix A, section 5](https://github.com/reagent-project/reagent/blob/master/doc/CreatingReagentComponents.md#appendix-a---lifting-the-lid-slightly)
46+
47+
That means reagent only calls `my-component` once. That call dereferences your subscription
48+
and then returns `my-render-fn`, which reagent calls "many, many times thereafter."
49+
But, `my-render-fn` uses `current-data`, an immutable binding which will always refer
50+
back to that first dereferenced value.
51+
52+
Put simply, **the solution is to dereference subscriptions within the actual render function**, not
53+
this constructor-like outer function. And, if it feels helpful, try using `*` to suggest
54+
that your symbol is bound to a derefable:
55+
56+
```clojure
57+
(defn my-component []
58+
(let [current-data* (rf/subscribe [:my-data])]
59+
(fn my-render-fn [] (str @current-data*))))
60+
```
61+
62+
Now, `my-render-fn` can get called "many, many times," and new values for `current-data*` will
63+
flow in, even as the lexical scope stays the same. It doesn't hurt to structure your original
64+
form-1 component in the same way, dereferencing subscriptions in the body form rather than in
65+
the `let` bindings.
66+
67+
Updating from a form-1 to a form-2 (or form-3) is an ordinary step in
68+
our development process - for instance, to declare some component-local state.
69+
But without a keen eye for reactivity, it's easy to break your components.
70+
71+
### You used `defmulti`
72+
73+
A subtle but severe problem can occur if you implement a form-2 component
74+
using `defmulti`/`defmethod`:
75+
76+
```clojure
77+
(defn which-fruit [k] k)
78+
79+
(defmulti fruit-view which-fruit)
80+
81+
(defmethod fruit-view :apple [_]
82+
[:li "🍎 I'm an apple!"])
83+
84+
(defmethod fruit-view :banana [_]
85+
(fn render-banana []
86+
[:li "🍌 I'm a banana!"]))
87+
88+
(def fruit (r/atom :apple))
89+
90+
(defn next-fruit! []
91+
(swap! fruit {:apple :banana
92+
:banana :apple}))
93+
94+
(defn fruit-switcher []
95+
[:div
96+
[:button {:on-click next-fruit!} "Click me!"]
97+
[:div "Showing " @fruit ":"]
98+
[fruit-view @fruit]])
99+
```
100+
101+
On the first render, reagent invokes `fruit-view`.
102+
Clojure runs your dispatch function `which-fruit`,
103+
looks up the method for `:apple`, and calls it, returning a hiccup.
104+
Since `fruit-view` returns a hiccup, reagent considers it to be a
105+
[form-1](https://github.com/reagent-project/reagent/blob/master/doc/CreatingReagentComponents.md#form-1-a-simple-function)
106+
component. It renders that hiccup directly. So far, so good.
107+
108+
Then, you click the button. The value of `fruit` changes, causing reagent to invoke
109+
`fruit-view` again. This time, we get the `:banana` method, which returns a `render-banana` (not a hiccup).
110+
Here's where the problem begins: since our `fruit-view` now returns a function,
111+
reagent now considers it to be
112+
[form-2](https://github.com/reagent-project/reagent/blob/master/doc/CreatingReagentComponents.md#form-2--a-function-returning-a-function)
113+
!
114+
115+
As explained above, reagent calls this new form-2 component function
116+
["once and once only"](https://github.com/reagent-project/reagent/blob/master/doc/CreatingReagentComponents.md#appendix-a---lifting-the-lid-slightly).
117+
Subsequently, every time `fruit-view` re-renders, reagent will _not_ call your multimethod,
118+
and no dispatch will happen. Instead, reagent has stored `render-banana` in its cache,
119+
and it will always call `render-banana`, showing "I'm a banana!" forever.
120+
121+
In effect, we've created a nasty dynamism where our component works perfectly well... until it doesn't.
122+
It could work for years, using only form-1 methods, until someone decides to add a form-2 to the codebase.
123+
124+
Unfortunately, we haven't found a perfect way to use multimethods with reagent.
125+
One workaround is to **use `defmulti` to implement only form-1 components.**
126+
127+
For instance, you could define your components using regular `defn`, and use `defmulti` only
128+
as a wrapper to dispatch them:
129+
130+
```clojure
131+
(defn fruit-item-apple [_]
132+
(fn [_] [:li "🍎 I'm an apple!"]))
133+
134+
(defmethod fruit-item :apple [props] [fruit-item-apple])
135+
136+
(defn fruit-item-banana [_]
137+
(fn [_] [:li "🍌 I'm a banana!"]))
138+
139+
(defmethod fruit-item :banana [props] [fruit-item-banana props])
140+
```
141+
142+
Or, you could extract your form-2 code into a single wrapper,
143+
and call your multimethod from there, making sure all your methods are form-1:
144+
145+
```clojure
146+
(defmethod fruit-item :apple [_]
147+
[:li "🍎 I'm an apple!"])
148+
149+
(defmethod fruit-item :banana [_]
150+
[:li "🍌 I'm a banana!"])
151+
152+
(defn fruit-view
153+
"We need this wrapper component to ensure reactivity.
154+
For details, refer to this article:
155+
https://day8.github.io/re-frame/FAQs/why-didnt-my-component-update"
156+
[k]
157+
(let [] ;; Do your form-2 stuff here.
158+
(fn []
159+
[fruit-item k])))
160+
```
161+
162+
If you truly need a polymorphic form-2 component, **consider not using `defmulti`**.
163+
Though multimethods are elegant, our workarounds can be clumsy.
164+
165+
**Consider using `case` or `cond`**, instead. This can work as idiomatic Clojure,
166+
providing explicit and foolproof dispatch without depending on
167+
incidental indirections like `fruit-view` above:
168+
169+
```clojure
170+
(defn fruit-item-apple []
171+
(fn [] ;; No problem using form-2
172+
[:li "🍎 I'm an apple!"]))
173+
174+
(defn fruit-item-banana []
175+
[:li "🍌 I'm a banana!"])
176+
177+
(defn fruit-view [k]
178+
(case k
179+
:apple [fruit-item-apple]
180+
:banana [fruit-item-banana]))
181+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ nav:
8686
- "Reagent looks terrific. Why do I need re-frame?": FAQs/DoINeedReFrame.md
8787
- "How to focus on an HTML element": FAQs/FocusOnElement.md
8888
- "How do I register a global interceptor": FAQs/GlobalInterceptors.md
89+
- "Why didn't my component update?": FAQs/why-didnt-my-component-update.md
8990
- "Why is my input field laggy?": FAQs/laggy-input.md
9091
- "How long after I do a dispatch does the event get handled?": FAQs/When-Does-Dispatch-Happen.md
9192
- "How can I use a subscription in an Event Handler": FAQs/UseASubscriptionInAnEventHandler.md

0 commit comments

Comments
 (0)