Make functional Reagent components optional, using new opts parameter

Render/as-element and other functions now take additional parameter for
setting options.

Currently only option is :functional-reag-elements?,
if set to true, by default Reagent components are
created as Functions, which behave similarly to Classes,
but allow using Hooks.
This commit is contained in:
Juho Teperi 2020-03-28 15:49:03 +02:00
parent 63e118d2a0
commit 22a2841b45
8 changed files with 1236 additions and 1112 deletions

View File

@ -46,8 +46,8 @@
(defn as-element
"Turns a vector of Hiccup syntax into a React element. Returns form
unchanged if it is not a vector."
[form]
(tmpl/as-element form))
([form] (tmpl/as-element form nil))
([form opts] (tmpl/as-element form opts)))
(defn adapt-react-class
"Returns an adapter for a native React class, that may be used
@ -60,9 +60,10 @@
"Returns an adapter for a Reagent component, that may be used from
React, for example in JSX. A single argument, props, is passed to
the component, converted to a map."
[c]
(assert-some c "Component")
(comp/reactify-component c))
([c] (reactify-component c nil))
([c opts]
(assert-some c "Component")
(comp/reactify-component c opts)))
(defn render
"Render a Reagent component into the DOM. The first argument may be

View File

@ -34,10 +34,13 @@
Returns the mounted component instance."
([comp container]
(render comp container nil))
([comp container callback]
([comp container callback-or-opts]
(ratom/flush!)
(let [f (fn []
(tmpl/as-element (if (fn? comp) (comp) comp)))]
(let [[opts callback] (if (fn? callback-or-opts)
[nil callback-or-opts]
[callback-or-opts (:callback callback-or-opts)])
f (fn []
(tmpl/as-element (if (fn? comp) (comp) comp) opts))]
(render-comp f container callback))))
(defn unmount-component-at-node

View File

@ -6,14 +6,18 @@
(defn render-to-string
"Turns a component into an HTML string."
[component]
(ratom/flush!)
(binding [util/*non-reactive* true]
(dom-server/renderToString (tmpl/as-element component))))
([component]
(render-to-string component nil))
([component opts]
(ratom/flush!)
(binding [util/*non-reactive* true]
(dom-server/renderToString (tmpl/as-element component opts)))))
(defn render-to-static-markup
"Turns a component into an HTML string, without data-react-id attributes, etc."
[component]
(ratom/flush!)
([component]
(render-to-static-markup component nil))
([component opts]
(ratom/flush!)
(binding [util/*non-reactive* true]
(dom-server/renderToStaticMarkup (tmpl/as-element component))))
(dom-server/renderToStaticMarkup (tmpl/as-element component opts)))))

View File

@ -111,10 +111,11 @@
5 (.call f c (nth v 1) (nth v 2) (nth v 3) (nth v 4))
(.apply f c (.slice (into-array v) 1)))))]
(cond
(vector? res) (as-element res)
;; FIXME: Opts
(vector? res) (as-element res nil)
(ifn? res) (let [f (if (reagent-class? res)
(fn [& args]
(as-element (apply vector res args)))
(as-element (apply vector res args) nil))
res)]
(set! (.-reagentRender c) f)
(recur c))
@ -373,7 +374,7 @@
cmp))
(defn fn-to-class [f]
(defn fn-to-class [f opts]
(assert-callable f)
(warn-unless (not (and (react-class? f)
(not (reagent-class? f))))
@ -389,15 +390,16 @@
res (create-class withrender)]
(cache-react-class f res))))
(defn as-class [tag]
(defn as-class [tag opts]
;; TODO: Cache per opts
(if-some [cached-class (cached-react-class tag)]
cached-class
(fn-to-class tag)))
(fn-to-class tag opts)))
(defn reactify-component [comp]
(defn reactify-component [comp opts]
(if (react-class? comp)
comp
(as-class comp)))
(as-class comp opts)))
(defn functional-wrap-render
[c]
@ -406,10 +408,10 @@
argv (.-argv c)
res (apply f argv)]
(cond
(vector? res) (as-element res)
(vector? res) (as-element res (.-opts c))
(ifn? res) (let [f (if (reagent-class? res)
(fn [& args]
(as-element (apply vector res args)))
(as-element (apply vector res args) (.-opts c)))
res)]
(set! (.-reagentRender c) f)
(recur c))
@ -501,6 +503,7 @@
original Reagent component."
[tag]
;; TODO: Could be disabled for optimized builds?
;; TODO: Need to cache per opts?
(or (gobj/get fun-components tag)
(let [f (fn [jsprops] (functional-render jsprops))
_ (set! (.-displayName f) (util/fun-name tag))

View File

@ -246,10 +246,10 @@
:component-did-update input-component-set-value
:component-will-unmount input-unmount
:reagent-render
(fn [argv component jsprops first-child]
(fn [argv component jsprops first-child opts]
(let [this comp/*current-component*]
(input-render-setup this jsprops)
(make-element argv component jsprops first-child)))})
(make-element argv component jsprops first-child opts)))})
(defn reagent-input
[]
@ -293,13 +293,21 @@
(if (= :> (nth v 0 nil))
(get-key (nth v 2 nil))))))
(defn reag-element [tag v]
(defn reag-element [tag v opts]
(let [c (comp/as-class tag opts)
jsprops #js {}]
(set! (.-argv jsprops) v)
(when-some [key (key-from-vec v)]
(set! (.-key jsprops) key))
(react/createElement c jsprops)))
(defn functional-reag-element [tag v opts]
(if (or (comp/react-class? tag)
;; TODO: Should check others for real comptibility, this fixes tests
;; TODO: Drop support for fn + meta for Class component methods?
(:should-component-update (meta tag)))
;; as-class unncessary later as tag is always class
(let [c (comp/as-class tag)
(let [c (comp/as-class tag opts)
jsprops #js {}]
(set! (.-argv jsprops) v)
(when-some [key (key-from-vec v)]
@ -308,6 +316,7 @@
(let [jsprops #js {}]
(set! (.-reagentRender jsprops) tag)
(set! (.-argv jsprops) (subvec v 1))
(set! (.-opts jsprops) opts)
(when-some [key (key-from-vec v)]
(set! (.-key jsprops) key))
(react/createElement (comp/funtional-render-fn tag) jsprops))))
@ -335,7 +344,7 @@
(gobj/set tag-name-cache x v)
v)))
(defn native-element [parsed argv first]
(defn native-element [parsed argv first opts]
(let [component (.-tag parsed)
props (nth argv first nil)
hasprops (or (nil? props) (map? props))
@ -343,13 +352,13 @@
#js {})
first-child (+ first (if hasprops 1 0))]
(if (input-component? component)
(-> [(reagent-input) argv component jsprops first-child]
(-> [(reagent-input) argv component jsprops first-child opts]
(with-meta (meta argv))
as-element)
(as-element opts))
(do
(when-some [key (-> (meta argv) get-key)]
(set! (.-key jsprops) key))
(make-element argv component jsprops first-child)))))
(make-element argv component jsprops first-child opts)))))
(defn str-coll [coll]
(if (dev?)
@ -365,7 +374,7 @@
(defn hiccup-err [v & msg]
(str (apply str msg) ": " (str-coll v) "\n" (comp/comp-name)))
(defn vec-to-elem [v]
(defn vec-to-elem [v opts]
(assert (pos? (count v)) (hiccup-err v "Hiccup form should not be empty"))
(let [tag (nth v 0 nil)]
(assert (valid-tag? tag) (hiccup-err v "Invalid Hiccup form"))
@ -377,53 +386,56 @@
(let [n (name tag)
pos (.indexOf n ">")]
(case pos
-1 (native-element (cached-parse n) v 1)
-1 (native-element (cached-parse n) v 1 opts)
0 (let [component (nth v 1 nil)]
;; Support [:> component ...]
(assert (= ">" n) (hiccup-err v "Invalid Hiccup tag"))
(native-element (->HiccupTag component nil nil nil) v 2))
(native-element (->HiccupTag component nil nil nil) v 2 opts))
;; Support extended hiccup syntax, i.e :div.bar>a.foo
;; Apply metadata (e.g. :key) to the outermost element.
;; Metadata is probably used only with sequeneces, and in that case
;; only the key of the outermost element matters.
(recur (with-meta [(subs n 0 pos)
(assoc (with-meta v nil) 0 (subs n (inc pos)))]
(meta v)))))
(meta v))
opts)))
(instance? NativeWrapper tag)
(native-element tag v 1)
(native-element tag v 1 opts)
:else (reag-element tag v))))
:else (if (:functional-reag-elements? opts)
(functional-reag-element tag v opts)
(reag-element tag v opts)))))
(declare expand-seq)
(declare expand-seq-check)
(defn as-element [x]
(defn as-element [x opts]
(cond (js-val? x) x
(vector? x) (vec-to-elem x)
(vector? x) (vec-to-elem x opts)
(seq? x) (if (dev?)
(expand-seq-check x)
(expand-seq x))
(expand-seq-check x opts)
(expand-seq x opts))
(named? x) (name x)
(satisfies? IPrintWithWriter x) (pr-str x)
:else x))
(set! comp/as-element as-element)
(defn expand-seq [s]
(defn expand-seq [s opts]
(into-array (map as-element s)))
(defn expand-seq-dev [s ^clj o]
(defn expand-seq-dev [s ^clj o opts]
(into-array (map (fn [val]
(when (and (vector? val)
(nil? (key-from-vec val)))
(set! (.-no-key o) true))
(as-element val))
(as-element val opts))
s)))
(defn expand-seq-check [x]
(defn expand-seq-check [x opts]
(let [ctx #js{}
[res derefed] (ratom/check-derefs #(expand-seq-dev x ctx))]
[res derefed] (ratom/check-derefs #(expand-seq-dev x ctx opts))]
(when derefed
(warn (hiccup-err x "Reactive deref not supported in lazy seq, "
"it should be wrapped in doall")))
@ -431,17 +443,17 @@
(warn (hiccup-err x "Every element in a seq should have a unique :key")))
res))
(defn make-element [argv component jsprops first-child]
(defn make-element [argv component jsprops first-child opts]
(case (- (count argv) first-child)
;; Optimize cases of zero or one child
0 (react/createElement component jsprops)
1 (react/createElement component jsprops
(as-element (nth argv first-child nil)))
(as-element (nth argv first-child nil) opts))
(.apply react/createElement nil
(reduce-kv (fn [a k v]
(when (>= k first-child)
(.push a (as-element v)))
(.push a (as-element v opts)))
a)
#js[component jsprops] argv))))

View File

@ -10,14 +10,14 @@
(js/performance.mark "functional-start")
; (simple-benchmark [x [hello-world-component]] (tmpl/vec-to-elem x) 100000)
(dotimes [i 100000]
(tmpl/vec-to-elem [hello-world-component]))
(tmpl/vec-to-elem [hello-world-component] nil))
(js/performance.mark "functional-end")
(js/performance.measure "functional" "functional-start" "functional-end")
(js/performance.mark "class-start")
; (simple-benchmark [x [^:class-component hello-world-component]] (tmpl/vec-to-elem x) 100000)
(dotimes [i 100000]
(tmpl/vec-to-elem [^:class-component hello-world-component]))
(tmpl/vec-to-elem [^:class-component hello-world-component] nil))
(js/performance.mark "class-end")
(js/performance.measure "class" "class-start" "class-end")
)

File diff suppressed because it is too large Load Diff

View File

@ -2,15 +2,18 @@
(:require [reagent.core :as r]
[reagent.dom :as rdom]))
(defn with-mounted-component [comp f]
(when r/is-client
(let [div (.createElement js/document "div")]
(try
(let [c (rdom/render comp div)]
(f c div))
(finally
(rdom/unmount-component-at-node div)
(r/flush))))))
(defn with-mounted-component
([comp f]
(with-mounted-component comp nil f))
([comp opts f]
(when r/is-client
(let [div (.createElement js/document "div")]
(try
(let [c (rdom/render comp div opts)]
(f c div))
(finally
(rdom/unmount-component-at-node div)
(r/flush)))))))
(defn with-mounted-component-async [comp done f]
(when r/is-client