Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add defc macro to create fn components with Fast Refresh support #598

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .clj-kondo/config.edn
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{:lint-as {reagent.core/with-let clojure.core/let
reagent.core/defc clojure.core/defn
reagenttest.utils/deftest clojure.test/deftest}
:linters {:unused-binding {:level :off}
:missing-else-branch {:level :off}
Expand Down
10 changes: 7 additions & 3 deletions demo/reagentdemo/dev.cljs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
(ns reagentdemo.dev
"Initializes the demo app, and runs the tests."
(:require [reagentdemo.core :as core]
(:require [reagent.dev]
[reagentdemo.core :as core]
[reagenttest.runtests :as tests]))

(reagent.dev/init-fast-refresh!)

(enable-console-print!)

(defn ^:dev/after-load init! []
(core/init! (tests/init!)))
(js/console.log (reagent.dev/refresh!))
(tests/init!))

(init!)
(defonce _init (core/init!))
36 changes: 18 additions & 18 deletions demo/reagentdemo/intro.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,44 @@
[simpleexample.core :as simple]
[todomvc.core :as todo]))

(defn simple-component []
(r/defc simple-component []
[:div
[:p "I am a component!"]
[:p.someclass
"I have " [:strong "bold"]
[:span {:style {:color "red"}} " and red "] "text."]])

(defn simple-parent []
(r/defc simple-parent []
[:div
[:p "I include simple-component."]
[simple-component]])

(defn hello-component [name]
(r/defc hello-component [name]
[:p "Hello, " name "!"])

(defn say-hello []
(r/defc say-hello []
[hello-component "world"])

(defn lister [items]
(r/defc lister [items]
[:ul
(for [item items]
^{:key item} [:li "Item " item])])

(defn lister-user []
(r/defc lister-user []
[:div
"Here is a list:"
[lister (range 3)]])

(def click-count (r/atom 0))

(defn counting-component []
(r/defc counting-component []
[:div
"The atom " [:code "click-count"] " has value: "
@click-count ". "
[:input {:type "button" :value "Click me!"
:on-click #(swap! click-count inc)}]])

(defn atom-input [value]
(r/defc atom-input [value]
[:input {:type "text"
:value @value
:on-change #(reset! value (-> % .-target .-value))}])
Expand All @@ -62,7 +62,7 @@
[:div
"Seconds Elapsed: " @seconds-elapsed])))

(defn render-simple []
(r/defc render-simple []
(rdom/render
[simple-component]
(.-body js/document)))
Expand All @@ -75,7 +75,7 @@

(def bmi-data (r/atom (calc-bmi {:height 180 :weight 80})))

(defn slider [param value min max invalidates]
(r/defc slider [param value min max invalidates]
[:input {:type "range" :value value :min min :max max
:style {:width "100%"}
:on-change (fn [e]
Expand All @@ -87,7 +87,7 @@
(dissoc invalidates)
calc-bmi)))))}])

(defn bmi-component []
(r/defc bmi-component []
(let [{:keys [weight height bmi]} @bmi-data
[color diagnose] (cond
(< bmi 18.5) ["orange" "underweight"]
Expand All @@ -114,7 +114,7 @@
(def ns-src-with-rdom (s/syntaxed "(ns example
(:require [reagent.dom :as rdom]))"))

(defn intro []
(r/defc intro []
(let [github {:href "https://github.com/reagent-project/reagent"}
clojurescript {:href "https://github.com/clojure/clojurescript"}
react {:href "https://reactjs.org/"}
Expand Down Expand Up @@ -176,7 +176,7 @@
is a map). See React’s " [:a react-keys "documentation"] "
for more info."]]))

(defn managing-state []
(r/defc managing-state []
[:div.demo-text
[:h2 "Managing state in Reagent"]

Expand Down Expand Up @@ -224,7 +224,7 @@
component is updated when your data changes. Reagent assumes by
default that two objects are equal if they are the same object."]])

(defn essential-api []
(r/defc essential-api []
[:div.demo-text
[:h2 "Essential API"]

Expand All @@ -240,7 +240,7 @@
ns-src-with-rdom
(s/src-of [:simple-component :render-simple])]}]])

(defn performance []
(r/defc performance []
[:div.demo-text
[:h2 "Performance"]

Expand Down Expand Up @@ -285,7 +285,7 @@
into the browser, React automatically attaches event-handlers to
the already present DOM tree."]])

(defn bmi-demo []
(r/defc bmi-demo []
[:div.demo-text
[:h2 "Putting it all together"]

Expand All @@ -301,7 +301,7 @@
(s/src-of [:calc-bmi :bmi-data :slider
:bmi-component])]}]])

(defn complete-simple-demo []
(r/defc complete-simple-demo []
[:div.demo-text
[:h2 "Complete demo"]

Expand All @@ -313,7 +313,7 @@
:complete true
:src (s/src-of nil "simpleexample/core.cljs")}]])

(defn todomvc-demo []
(r/defc todomvc-demo []
[:div.demo-text
[:h2 "Todomvc"]

Expand Down
8 changes: 8 additions & 0 deletions doc/ReactRefresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# React Refresh

- `reagent.dev` ns must be required before anything that loads `react-dom`
- Don't call `r.dom/render` after reload (e.g. shadow-cljs hook)
- Call `reagent.dev/refresh!` instead
- Only components defined using `r/defc` will refresh
- Reagent doesn't try to create Hook signatures for components,
so hook state is reset for updated components.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m curious, if local state will be reset, what’s the motivation to enable react refresh in Reagent?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To help projects using both Reagent and UIx/Helix, so they can enable it and get benefit on React Refresh for UIx/Helix components.

16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "@reagent-project/reagent",
"private": true,
"dependencies": {},
"scripts": {
"start": "lein figwheel client-npm"
},
Expand All @@ -16,11 +15,15 @@
"karma-sourcemap-loader": "0.3.8",
"karma-webpack": "5.0.0",
"md5-file": "5.0.0",
"shadow-cljs": "2.20.7",
"webpack": "5.65.0",
"webpack-cli": "4.9.1",
"prop-types": "15.8.1",
"react": "18.2.0",
"react-dom": "18.2.0"
"react-dom": "18.2.0",
"react-refresh": "^0.14.0",
"shadow-cljs": "2.20.7",
"webpack": "5.65.0",
"webpack-cli": "4.9.1"
},
"volta": {
"node": "18.15.0"
}
}
55 changes: 54 additions & 1 deletion src/reagent/core.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns reagent.core
(:require [reagent.ratom :as ra]))
(:require [cljs.core :as core]
[reagent.ratom :as ra]))

(defmacro with-let
"Bind variables as with let, except that when used in a component
Expand All @@ -24,3 +25,55 @@
[& body]
`(reagent.ratom/make-reaction
(fn [] ~@body)))

;; From uix.lib
(defn parse-sig [name fdecl]
(let [[fdecl m] (if (string? (first fdecl))
[(next fdecl) {:doc (first fdecl)}]
[fdecl {}])
[fdecl m] (if (map? (first fdecl))
[(next fdecl) (conj m (first fdecl))]
[fdecl m])
fdecl (if (vector? (first fdecl))
(list fdecl)
fdecl)
[fdecl m] (if (map? (last fdecl))
[(butlast fdecl) (conj m (last fdecl))]
[fdecl m])
m (conj {:arglists (list 'quote (#'cljs.core/sigs fdecl))} m)
m (conj (if (meta name) (meta name) {}) m)]
[(with-meta name m) fdecl]))

(defmacro defc
"Creates function component"
[sym & fdecl]
;; Just use original fdecl always for render fn.
;; Parse for fname metadata.
(let [[fname fdecl] (parse-sig sym fdecl)
;; FIXME: Should probably support multiple arities for components
[args & fdecl] (first fdecl)
var-sym (-> (str (-> &env :ns :name) "/" sym) symbol (with-meta {:tag 'js}))]
`(do
(def ~fname (reagent.impl.component/memo
(fn ~fname [jsprops#]
;; FIXME: Replace functional-render with new function that takes renderFn as parameter.
;; Need completely new component impl for that.
(let [render-fn# (fn ~'reagentRender ~args
(when ^boolean goog.DEBUG
(when-let [sig-f# (.-fast-refresh-signature ~var-sym)]
(sig-f#)))
~@fdecl)
jsprops2# (js/Object.assign (core/js-obj "reagentRender" render-fn#) jsprops#)]
(reagent.impl.component/functional-render reagent.impl.template/*current-default-compiler* jsprops2#)))))
(reagent.dev/register ~var-sym ~(str fname))
(when ^boolean goog.DEBUG
(let [sig# (reagent.dev/signature)]
;; Empty signature but set forceReset flag.
(sig# ~var-sym "" true nil)
(set! (.-fast-refresh-signature ~var-sym) sig#)))
(set! (.-reagent-component ~fname) true)
(set! (.-displayName ~fname) ~(str sym)))))

(comment
(clojure.pprint/pprint (macroexpand-1 '(defc foobar [a b] (+ a b))))
(clojure.pprint/pprint (clojure.walk/macroexpand-all '(defc foobar [a b] (+ a b)))))
1 change: 1 addition & 0 deletions src/reagent/dev.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(ns reagent.dev)
21 changes: 21 additions & 0 deletions src/reagent/dev.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
(ns reagent.dev
(:require ["react-refresh/runtime" :as refresh])
(:require-macros [reagent.dev]))

(defn signature []
(refresh/createSignatureFunctionForTransform))

(defn register [type id]
(refresh/register type id))

;;;; Public API ;;;;

(defn init-fast-refresh!
"Injects react-refresh runtime. Should be called before UI is rendered"
[]
(refresh/injectIntoGlobalHook js/window))

(defn refresh!
"Should be called after hot-reload, in shadow's ^:dev/after-load hook"
[]
(refresh/performReactRefresh))
5 changes: 5 additions & 0 deletions src/reagent/impl/component.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,8 @@
f (react/memo f functional-render-memo-fn)]
(cache-react-class compiler tag f)
f)))

;; defc impl

(defn memo [f]
(react/memo f functional-render-memo-fn))
15 changes: 13 additions & 2 deletions src/reagent/impl/template.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@
(or (named? x)
(string? x)))

(defn ^boolean valid-tag? [x]
(defn ^boolean valid-tag? [^clj x]
(or (hiccup-tag? x)
(.-reagent-component x)
(ifn? x)
(instance? NativeWrapper x)))

Expand Down Expand Up @@ -162,6 +163,13 @@
(set! (.-key jsprops) key))
(react/createElement c jsprops)))

(defn reag-element-2 [tag v]
(let [jsprops #js {}]
(set! (.-argv jsprops) (subvec v 1))
(when-some [key (util/react-key-from-vec v)]
(set! (.-key jsprops) key))
(react/createElement tag jsprops)))

(defn function-element [tag v first-arg compiler]
(let [jsprops #js {}]
(set! (.-reagentRender jsprops) tag)
Expand Down Expand Up @@ -276,14 +284,17 @@
(when (nil? compiler)
(js/console.error "vec-to-elem" (pr-str v)))
(assert (pos? (count v)) (util/hiccup-err v (comp/comp-name) "Hiccup form should not be empty"))
(let [tag (nth v 0 nil)]
(let [^clj tag (nth v 0 nil)]
(assert (valid-tag? tag) (util/hiccup-err v (comp/comp-name) "Invalid Hiccup form"))
(case tag
:> (native-element (->HiccupTag (nth v 1 nil) nil nil nil) v 2 compiler)
:r> (raw-element (nth v 1 nil) v compiler)
:f> (function-element (nth v 1 nil) v 2 compiler)
:<> (fragment-element v compiler)
(cond
(.-reagent-component tag)
(reag-element-2 tag v)

(hiccup-tag? tag)
(hiccup-element v compiler)

Expand Down