/
multiple_roots_renderer.cljc
238 lines (206 loc) · 11.4 KB
/
multiple_roots_renderer.cljc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
(ns com.fulcrologic.fulcro.rendering.multiple-roots-renderer
"Like keyframe-render2, but also supports free-floating roots.
WARNING: THIS RENDERER SHOULD BE CONSIDERED DEPRECATED (even though for legacy reasons it is the default)
in favor of using hooks with helpers like `use-component`.
General usage:
1. Set this nses `render!` as your application's optimized render function.
2. Create a class that follows all of the normal rules for a Fulcro root (no ident, has initial state,
composes children queries/initial-state, etc.
a. Add mount/unmount register/deregister calls
2. Use floating-root-factory to generate a Fulcro factory, or floating-root-react-class to generate
a vanilla React wrapper class that renders the new root.
a. Use the factory in normal Fuclro rendering, but don't pass it props, or
b. Use `(dom/create-element ReactClass)` to render the vanilla wrapper, or
c. Use the vanilla wrapper class when a js library controls rendering (like routing).
Example:
```
(defonce app (app/fulcro-app {:optimized-render! mroot/render!}))
(defsc AltRoot [this {:keys [alt-child]}]
;; query is from ROOT of the db, just like normal root.
{:query [{:alt-child (comp/get-query OtherChild)}]
:componentDidMount (fn [this] (mroot/register-root! this {:app app}))
:componentWillUnmount (fn [this] (mroot/deregister-root! this {:app app}))
:shouldComponentUpdate (fn [] true)
:initial-state {:alt-child [{:id 1 :n 22}
{:id 2 :n 44}]}}
(dom/div
(mapv ui-other-child alt-child)))
;; For use in the body of normal defsc components.
(def ui-alt-root (mroot/floating-root-factory AltRoot))
;; For use as plain React class
(def PlainAltRoot (mroot/floating-root-react-class AltRoot app))
...
(some-js-library #js {:thing PlainAltRoot})
(defsc NormalFulcroClass [this props]
{:query [:stuff]
:ident (fn [] [:x 1])
...}
(dom/div
;; ok to use within defsc components:
(ui-alt-root)
;; how to use the plain react class, which is how js libs would use it:
(dom/create-element PlainAltRoot)))
```
"
#?(:cljs (:require-macros [com.fulcrologic.fulcro.rendering.multiple-roots-renderer :refer [with-app-context]]))
(:require
[com.fulcrologic.fulcro.algorithms.denormalize :as fdn]
[com.fulcrologic.fulcro.algorithms.lookup :as ah]
[com.fulcrologic.fulcro.components :as comp]
[com.fulcrologic.fulcro.raw.application :as rapp]
[com.fulcrologic.fulcro.rendering.ident-optimized-render :as ior]
[com.fulcrologic.fulcro.rendering.keyframe-render :as kr]
[edn-query-language.core :as eql]
[taoensso.timbre :as log]
#?@(:cljs
[["react" :as react]
[goog.object :as gobj]])))
(defn register-root!
"DEPRECATED: Floating roots can now be created with React hooks and hooks/use-component or hooks/use-fulcro.
Register a mounted react component as a new root that should be managed. The
options map can contain:
- `:initialize?`: Should the initial state be pushed into the app state (if not already present)? Defaults
to true, which causes it to happen once (on initial mount)."
([react-instance]
(register-root! react-instance {:initialize? true}))
([react-instance {:keys [app initialize?]}]
(let [app (or app (comp/any->app react-instance) comp/*app*)]
(if (map? app)
(let [class (comp/react-type react-instance)
k (comp/class->registry-key class)
initialize-state! (ah/app-algorithm app :initialize-state!)
schedule-render! (ah/app-algorithm app :schedule-render!)
known-roots (some-> app :com.fulcrologic.fulcro.application/runtime-atom deref ::known-roots)
initialized? (contains? known-roots k)]
(when (and initialize? (not initialized?))
(initialize-state! app class)
;; We've already rendered this frame, so if we need initialized, we need a refresh
(schedule-render! app {:force-root? true}))
(log/debug "Adding root of type " k)
(swap! (:com.fulcrologic.fulcro.application/runtime-atom app) update-in [::known-roots k] (fnil conj #{}) react-instance))
(log/error "Register-root cannot find app. Pass your Fulcro app via options. See https://book.fulcrologic.com/#err-mrr-reg-root-no-app")))))
(defn deregister-root!
"Deregister a mounted root that should no longer be managed."
([react-instance]
(deregister-root! react-instance {}))
([react-instance {:keys [app] :as options}]
(let [app (or app (comp/any->app react-instance) comp/*app*)]
(if (map? app)
(let [class (comp/react-type react-instance)
k (comp/class->registry-key class)]
(log/debug "Adding root of type " k)
(swap! (:com.fulcrologic.fulcro.application/runtime-atom app) update-in [::known-roots k] disj react-instance))
(log/error "Deregister-root cannot find app. Pass your Fulcro app via options. See https://book.fulcrologic.com/#err-mrr-dereg-root-no-app")))))
(defn render-roots! [app options]
(let [state-map (some-> app :com.fulcrologic.fulcro.application/state-atom deref)
known-roots (some-> app :com.fulcrologic.fulcro.application/runtime-atom deref ::known-roots)]
(kr/render! app options)
(doseq [k (keys known-roots)
:let [cls (comp/registry-key->class k)
query (comp/get-query cls state-map)
root-props (fdn/db->tree query state-map state-map)]]
(doseq [root (get known-roots k)]
(comp/tunnel-props! root root-props)))))
(defn render-stale-components!
"This function tracks the state of the app at the time of prior render in the app's runtime-atom. It
uses that to do a comparison of old vs. current application state (bounded by the needs of on-screen components).
When it finds data that has changed it renders all of the components that depend on that data."
[app options]
(let [{:com.fulcrologic.fulcro.application/keys [runtime-atom]} app
{:com.fulcrologic.fulcro.application/keys [only-refresh]} @runtime-atom
limited-refresh? (seq only-refresh)]
(if limited-refresh?
(let [{limited-idents true} (group-by eql/ident? only-refresh)]
(doseq [i limited-idents]
(ior/render-components-with-ident! app i)))
(render-roots! app options))))
(defn render!
"DEPRECATED: Floating roots can now be created with React hooks and hooks/use-component or hooks/use-fulcro.
The top-level call for using this optimized render in your application.
If `:force-root? true` is passed in options, then it just forces a keyframe root render.
This renderer always does a keyframe render *unless* an `:only-refresh` option is passed to the stack
(usually as an option on `(transact! this [(f)] {:only-refresh [...idents...]})`. In that case the renderer
will ignore *all* data diffing and will target refresh only to the on-screen components that have the listed
ident(s). This allows you to get component-local state refresh rates on transactions that are responding to
events that should really only affect a known set of components (like the input field).
This option does *not* currently support using query keywords in the refresh set. Only idents."
([app]
(render! app {}))
([app {:keys [force-root? root-props-changed?] :as options}]
(if (or force-root? root-props-changed?)
(render-roots! app options)
(try
(render-stale-components! app options)
(catch #?(:clj Exception :cljs :default) e
(log/info "Optimized render failed. Falling back to root render.")
(render-roots! app options))))))
#?(:clj
(defmacro with-app-context
"Wraps the given body with the correct internal bindings of the given fulcro-app so that Fulcro internals
will work when that body is embedded in unusual ways.
You should use this around the render body of any floating root that will be rendered outside of
the synchronous fulcro render (e.g. you pass a floating root class to a React library).
"
[fulcro-app & body]
(if-not (:ns &env)
`(do ~@body)
`(let [app# (or comp/*app* ~fulcro-app)]
(binding [comp/*app* app#
comp/*shared* (comp/shared app#)]
~@body)))))
(defn floating-root-react-class
"Generate a plain React class that can render a Fulcro UIRoot. NOTE: The UIRoot must register/deregister itself
in the component lifecycle:
```
(defsc UIRoot [this props]
{:componentDidMount (fn [this] (mroot/register-root! this))
:componentWillUnmount (fn [this] (mroot/deregister-root! this))
:initial-state {}
:query [root-like-query]}
...)
```
The `fulcro-app` is the app under which this root will be rendered. Create different factories if you have more than
one mounted app.
"
[UIRoot fulcro-app]
(let [cls (fn [])
ui-root (comp/computed-factory UIRoot)]
#?(:cljs
(gobj/extend (.-prototype cls) (.-prototype react/Component)
(clj->js
{:shouldComponentUpdate (fn [] false)
:render (fn []
(this-as ^js this
(let [js-props (.-props this)]
(with-app-context fulcro-app
(let [state-map (some-> fulcro-app :com.fulcrologic.fulcro.application/state-atom deref)
query (comp/get-query UIRoot state-map)
props (fdn/db->tree query state-map state-map)]
(ui-root props {:js-props js-props}))))))})))
cls))
(defn floating-root-factory
"Create a factory that renders a floating root in a normal Fulcro context (body of a Fulcro component). This factory
has the same sync constraints as normal `component/factory` functions. See `components/with-parent-context`.
`UIClass`: A class that will behave as a floating root. NOTE: that class MUST have a mount/unmount hook
to regsiter/deregister itself as a root.
`options`: An options map. Same as for `component/factory`. Note, however, that this factory will *not* receive
props, so a `:keyfn` would have to be based on something else.
You normally do not pass any props to this factory because it is controlling the component and feeding props from
the database. Props sent to this factory are only used by the wrapper, however, `:react-key` is useful if you
have a bunch of sibling roots and need to set the react key for each.
"
([UIClass]
(floating-root-factory UIClass {}))
([UIClass options]
(let [constructor (fn [])
ui-factory (comp/computed-factory UIClass)
render (fn [this]
(let [state-map (some-> comp/*app* :com.fulcrologic.fulcro.application/state-atom deref)
query (comp/get-query UIClass state-map)
props (fdn/db->tree query state-map state-map)]
(ui-factory (or props {}) (comp/props this))))
wrapper-class (comp/configure-component! constructor ::wrapper
{:shouldComponentUpdate (fn [_ _ _] false)
:render render})
wrapper-factory (comp/factory wrapper-class options)]
wrapper-factory)))