Skip to content

Commit

Permalink
Add defc macro to create fn components with Fast Refresh support
Browse files Browse the repository at this point in the history
  • Loading branch information
Deraen committed Dec 1, 2023
1 parent a14faba commit 48e4bfd
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 31 deletions.
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
12 changes: 9 additions & 3 deletions demo/reagentdemo/dev.cljs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
(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!)))
; (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
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"
}
}
54 changes: 53 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,54 @@
[& 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)
[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))
12 changes: 10 additions & 2 deletions src/reagent/impl/component.cljs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns reagent.impl.component
(:require [goog.object :as gobj]
(:require [clojure.string :as str]
[goog.object :as gobj]
[react :as react]
[reagent.impl.util :as util]
[reagent.impl.batching :as batch]
Expand Down Expand Up @@ -360,7 +361,8 @@
(cache-react-class compiler f f)
(let [spec (meta f)
withrender (assoc spec :reagent-render f)
res (create-class withrender compiler)]
res (create-class withrender compiler)
display-name (.-displayName res)]
(cache-react-class compiler f res))))

(defn as-class [tag compiler]
Expand All @@ -376,6 +378,7 @@
(defn functional-wrap-render
[compiler ^clj c]
(let [f (.-reagentRender c)
; _ (js/console.log "render" c)
_ (assert-callable f)
argv (.-argv c)
res (apply f argv)]
Expand Down Expand Up @@ -477,3 +480,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))

0 comments on commit 48e4bfd

Please sign in to comment.