Skip to content

Commit 06f3c10

Browse files
committed
Add clj value handling to hooks
1 parent 9579fd6 commit 06f3c10

File tree

2 files changed

+102
-10
lines changed

2 files changed

+102
-10
lines changed

src/reagent/hooks.cljs

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,81 @@
11
(ns reagent.hooks
22
(:require ["react" :as react]))
33

4+
;; Helpers to make hooks use clojure value equality etc.
5+
;; These are copied from (with permission) or inspired by UIx implementation
6+
7+
(defn with-return-value-check
8+
"Consider any non-fn value from effect callbacks as no callback."
9+
[f]
10+
(fn []
11+
(let [ret (f)]
12+
(if (fn? ret)
13+
ret
14+
js/undefined))))
15+
16+
(defn use-clj-deps
17+
"Check the new clj deps value against the previous value, stored in a
18+
ref hook, so we can use clj equality check to see if the value changed.
19+
20+
Only not= value is stored to the ref, so the effect dependency value
21+
only updates when the equality changed."
22+
[deps]
23+
(let [ref (react/useRef deps)]
24+
(when (not= (.-current ref) deps)
25+
(set! (.-current ref) deps))
26+
(.-current ref)))
27+
28+
(defn- use-clojure-aware-updater
29+
"When update fn returns the equal clj value as the previous value,
30+
keep using the old value, to keep the identity stable."
31+
[updater]
32+
(react/useCallback
33+
(fn [v & args]
34+
(updater
35+
(fn [current-value]
36+
(let [new-value (if (fn? v)
37+
(apply v current-value args)
38+
v)]
39+
(js/console.log (pr-str current-value) (pr-str new-value) (= new-value current-value))
40+
(if (= new-value current-value)
41+
current-value
42+
new-value)))))
43+
#js [updater]))
44+
45+
;; Hooks
46+
447
(defn use-state [initial-state]
5-
(react/useState initial-state))
48+
(let [[state set-state] (react/useState initial-state)
49+
set-state (use-clojure-aware-updater set-state)]
50+
#js [state set-state]))
651

752
(defn use-reducer [reducer initial-arg init]
8-
(react/useReducer reducer initial-arg init))
53+
(react/useReducer (fn [state action]
54+
(let [new-state (reducer state action)]
55+
(if (= new-state state)
56+
state
57+
new-state)))
58+
initial-arg
59+
init))
960

1061
(defn use-ref [v]
1162
(react/useRef v))
1263

1364
(defn use-effect [setup dependencies]
14-
(react/useEffect setup dependencies))
65+
(react/useEffect (with-return-value-check setup)
66+
(array (use-clj-deps dependencies))))
1567

1668
(defn use-layout-effect [setup dependencies]
17-
(react/useLayoutEffect setup dependencies))
69+
(react/useLayoutEffect (with-return-value-check setup)
70+
(array (use-clj-deps dependencies))))
1871

1972
(defn use-memo [calculate-value dependencies]
20-
(react/useMemo calculate-value dependencies))
73+
(react/useMemo calculate-value
74+
(array (use-clj-deps dependencies))))
2175

2276
(defn use-callback [f dependencies]
23-
(react/useCallback f dependencies))
77+
(react/useCallback f
78+
(array (use-clj-deps dependencies))))
2479

2580
(defn use-context [ctx]
2681
(react/useContext ctx))

test/reagenttest/testhooks.cljs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
(def ran (atom 0))
1515
(def update-state (atom nil))
1616

17-
(r/defc foo1 []
17+
(r/defc state-1 []
1818
(let [[x set-x] (hooks/use-state 0)]
1919
(reset! update-state set-x)
2020
(swap! ran inc)
2121
[:div "foo " x]))
2222

23-
(r/defc foo2 []
23+
(r/defc state-2 []
2424
(let [[x set-x] (hooks/use-state {:foo 0})]
2525
(reset! update-state set-x)
2626
(swap! ran inc)
@@ -29,7 +29,8 @@
2929
(deftest ^:dom use-state-test
3030
(u/async
3131
(testing "update state, pure value"
32-
(u/with-render [div [foo1]]
32+
(reset! ran 0)
33+
(u/with-render [div [state-1]]
3334
(is (= 1 @ran))
3435
(is (= "foo 0" (.-innerText div)))
3536

@@ -39,7 +40,7 @@
3940

4041
(testing "update state, clojure value"
4142
(reset! ran 0)
42-
(u/with-render [div [foo2]]
43+
(u/with-render [div [state-2]]
4344
(is (= 1 @ran))
4445
(is (= "foo 0" (.-innerText div)))
4546

@@ -53,3 +54,39 @@
5354
(is (= 2 @ran))
5455
(is (= "foo 1" (.-innerText div)))))))
5556

57+
(def effect-ran (atom 0))
58+
59+
(r/defc effect-1 [x]
60+
(hooks/use-effect (fn []
61+
(swap! effect-ran inc)
62+
nil)
63+
[x])
64+
(swap! ran inc)
65+
[:div "foo " (:foo x)])
66+
67+
(deftest ^:dom use-effect-test
68+
(u/async
69+
(testing "update state, pure value"
70+
(reset! ran 0)
71+
(reset! effect-ran 0)
72+
(let [x (r/atom {:foo 0})
73+
y (r/atom 0)
74+
c (fn []
75+
[effect-1 @x @y])]
76+
(u/with-render [div [c]]
77+
(is (= 1 @ran))
78+
(is (= 1 @effect-ran))
79+
(is (= "foo 0" (.-innerText div)))
80+
81+
(u/act (swap! x assoc :foo 1))
82+
(is (= 2 @ran))
83+
(is (= 2 @effect-ran))
84+
(is (= "foo 1" (.-innerText div)))
85+
86+
;; use-effect wrapper considers clojure equality
87+
(u/act (swap! x assoc :foo 1)
88+
(swap! y inc))
89+
(is (= 3 @ran))
90+
;; effect didn't ran again because dependency value is the same
91+
(is (= 2 @effect-ran))
92+
(is (= "foo 1" (.-innerText div))))))))

0 commit comments

Comments
 (0)