Skip to content

Commit b604831

Browse files
authored
Merge pull request #647 from reagent-project/class-strict-mode-fix
Fix render ratom watches being lost for class components with StrictMode
2 parents 6f645cf + 4101d1a commit b604831

File tree

3 files changed

+73
-27
lines changed

3 files changed

+73
-27
lines changed

src/reagent/impl/component.cljs

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,30 @@
116116
(binding [*current-component* c]
117117
(wrap-render c compiler)))
118118

119+
;; Ratom cleanup
120+
;; On StrictMode React calls mount->unmount->mount on component initialization.
121+
;; This means we need to use tricks to keep the render ratom running even
122+
;; though unmount is called on initialization. It we let the dispose to be
123+
;; called, we lose the ratom watches from derefs inside reagent-render, and
124+
;; there is no good way to restore them.
125+
126+
(defn cancel-cleanup [^clj component-state]
127+
(when (.-cleanup-queued component-state)
128+
(set! (.-cleanup-cancelled component-state) true)
129+
(set! (.-cleanup-queued component-state) false)))
130+
131+
;; Delay ratom dispose call so it can be cancelled if mount/setup is second time.
132+
(defn queue-cleanup [^clj component-state]
133+
(set! (.-cleanup-cancelled component-state) false)
134+
(set! (.-cleanup-queued component-state) true)
135+
;; Promise.resolve creates a microtask, vs setTimeout regular task.
136+
;; A scheduled microtask runs before the next event loop begings (where a regular task would run).
137+
(.then (.resolve js/Promise nil)
138+
(fn []
139+
(when (false? (.-cleanup-cancelled component-state))
140+
(set! (.-cleanup-queued component-state) false)
141+
(batch/mark-rendered component-state)
142+
(some-> (gobj/get component-state "cljsRatom") ratom/dispose!)))))
119143

120144
;;; Method wrapping
121145

@@ -193,13 +217,15 @@
193217

194218
:componentDidMount
195219
(fn componentDidMount []
196-
(this-as c (.call f c c)))
220+
(this-as c
221+
(cancel-cleanup c)
222+
(when-not (nil? f)
223+
(.call f c c))))
197224

198225
:componentWillUnmount
199226
(fn componentWillUnmount []
200227
(this-as c
201-
(some-> (gobj/get c "cljsRatom") ratom/dispose!)
202-
(batch/mark-rendered c)
228+
(queue-cleanup c)
203229
(when-not (nil? f)
204230
(.call f c c))))
205231

@@ -218,6 +244,7 @@
218244
;; Though the value is nil here, the wrapper function will be
219245
;; added to class to manage Reagent ratom lifecycle.
220246
(def obligatory {:shouldComponentUpdate nil
247+
:componentDidMount nil
221248
:componentWillUnmount nil})
222249

223250
(def dash-to-method-name (util/memoize-1 util/dash-to-method-name))
@@ -427,26 +454,11 @@
427454
;; FIXME: Access cljsRatom using interop forms
428455
rat ^ratom/Reaction (gobj/get reagent-state "cljsRatom")]
429456

430-
;; Delay ratom dispose call so it can be cancelled if StrictMode
431-
;; calls setup the second time.
432457
(react/useEffect
433458
(fn mount []
434-
(when (.-cleanup-queued reagent-state)
435-
;; (js/console.log "cancel cleanup")
436-
(set! (.-cleanup-cancelled reagent-state) true)
437-
(set! (.-cleanup-queued reagent-state) false))
459+
(cancel-cleanup reagent-state)
438460
(fn unmount []
439-
;; (js/console.log "queue cleanup")
440-
(set! (.-cleanup-cancelled reagent-state) false)
441-
(set! (.-cleanup-queued reagent-state) true)
442-
;; Promise.resolve creates a microtask, vs setTimeout regular task.
443-
;; A scheduled microtask runs before the next event loop begings (where a regular task would run).
444-
(.then (.resolve js/Promise nil)
445-
(fn []
446-
(when (false? (.-cleanup-cancelled reagent-state))
447-
;; (js/console.log "run cleanup")
448-
(set! (.-cleanup-queued reagent-state) false)
449-
(some-> (gobj/get reagent-state "cljsRatom") ratom/dispose!))))))
461+
(queue-cleanup reagent-state)))
450462
;; Ignore props - only run effect once on mount and unmount
451463
;; (which means always twice under the React strict mode).
452464
#js [])

test/reagenttest/testreagent.cljs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,36 @@
14481448
;; TODO: Test that component RAtom is disposed
14491449
(is (= "Count 6" (.-innerText div)))))))
14501450

1451+
(deftest ^:dom class-strict
1452+
;; Ensure mount->unmount->mount done by StrictMode
1453+
;; doesn't prematurely dispose the render ratom
1454+
(let [numbers (r/atom [0 1])
1455+
num-c (fn [i]
1456+
(let [x (get @numbers i)]
1457+
[:span x]))
1458+
c (fn []
1459+
(let [xs @numbers]
1460+
[:span
1461+
"counting "
1462+
(for [i (range (count xs))]
1463+
^{:key i}
1464+
[num-c i])]))
1465+
runs (rv/running)]
1466+
(u/async
1467+
(u/with-render [div [c]]
1468+
{:compiler u/class-compiler
1469+
:strict? true}
1470+
1471+
(is (= "counting 01" (.-innerText div)))
1472+
1473+
(u/act (reset! numbers [2 4 6]))
1474+
1475+
(is (= "counting 246" (.-innerText div))))
1476+
1477+
(testing "after unmount ratom watches are cleaned"
1478+
(is (= {} (.-watches ^clj numbers)))
1479+
(is (= runs (rv/running)))))))
1480+
14511481
(deftest ^:dom functional-strict
14521482
(let [numbers (r/atom [0 1])
14531483
num-c (fn [i]
@@ -1459,7 +1489,7 @@
14591489
ref (react/useRef nil)]
14601490
[:span
14611491
"counting "
1462-
(for [i xs]
1492+
(for [i (range (count xs))]
14631493
^{:key i}
14641494
[num-c i])]))
14651495
runs (rv/running)]
@@ -1480,8 +1510,8 @@
14801510
(is (= runs (rv/running)))))))
14811511

14821512
(deftest ^:dom functional-strict-transitive
1483-
(let [count (r/atom 2)
1484-
numbers (rv/reaction (vec (range @count)))
1513+
(let [cnt (r/atom 2)
1514+
numbers (rv/reaction (vec (range @cnt)))
14851515
num-c (fn [i]
14861516
(let [x (get @numbers i)]
14871517
[:span x]))
@@ -1491,7 +1521,7 @@
14911521
ref (react/useRef nil)]
14921522
[:span
14931523
"counting "
1494-
(for [i xs]
1524+
(for [i (range (count xs))]
14951525
^{:key i}
14961526
[num-c i])]))
14971527

@@ -1502,11 +1532,11 @@
15021532
:capture-errors true
15031533
:strict? true}
15041534

1505-
(u/act (reset! count 3))
1535+
(u/act (reset! cnt 3))
15061536
(is (= "counting 012" (.-innerText div)))
15071537
(is (= [0 1 2] @numbers))
15081538

1509-
(u/act (reset! count 2))
1539+
(u/act (reset! cnt 2))
15101540
(is (= "counting 01" (.-innerText div)))
15111541

15121542
(when (dev?)

test/reagenttest/utils.cljs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
(resolve))
116116
RENDER-WAIT)
117117
(catch :default e
118+
;; NOTE: errors from act body aren't logged currently
118119
(reject e))))))
119120

120121
(def ^:dynamic *render-error* nil)
@@ -137,7 +138,10 @@
137138
(init-capture))
138139
;; Magic setup to make exception from render available to the
139140
;; with-render body.
140-
render-error (atom nil)]
141+
render-error (atom nil)
142+
comp (if (:strict-mode? options)
143+
[:> react/StrictMode comp]
144+
comp)]
141145
(try
142146
(if compiler
143147
(rdom/render comp div {:compiler compiler

0 commit comments

Comments
 (0)