Comments (11)
Really appreciate the quick response, thank you! I'm clearly still confused about some of the fundamental concepts here :-) Could I possibly check my understanding of a few things?:
- A React "component" is an instance of a the ReactComponent class and implements the relevant lifecycle API methods (notably
render
). - A Reagent "component" is == to a React component.
- Reagent creates components from Hiccup forms like
[:div "hello"]
or[my-function "world"]
.
Let's say that (defn my-function [s] [:div s])
:
[:div "hello" [:div "world"]]
will create two components: a:div
type, and a:div
type.[:div "hello" [my-function "world"]]
will create three components: a:div
type, amy-function
type, and a:div
type.- The
[my-function "world"]
form's rendering will be cached based on its arguments (just "world" in this case). [:div "hello" (my-function "world")]
will create two components: a:div
type and a:div
type. That's what you meant by "just inlines" above.- The
(my-function "world")
form will be reevaluated on every render (there's no render caching). - We should prefer
(my-function "world")
over[my-function "world"]
when we don't expect the arguments (just "world" in this case) to change. - We should prefer
[my-function "world"]
over(my-function "world")
when we expect the arguments (just "world" in this case) to change. - We'll never need/want a
:key
on[my-function "world"]
unless we're actually doing something like:
(for [text ["world" "universe" "multiverse"]] [my-function {:key text} text])
Do I have that all right?
So I was initially under the impression that [:div "hello" [my-function "world"]]
would create two not three components, which is why the :key
behaviour was surprising me. I figured there was a 1-to-1 relationship between components and dom elements, but that's not actually the case is it?
So my thinking should go - any time I see [<foo>
, there's a component of type <foo>
being created, even when <foo>
is a function. If I want inlining behaviour, I need (<foo>
.
One last question:
If [my-function <arguments>]
's rendering is cached, do we not face an issue where the cache just grows without bounds when the argument input domain is unbounded?
For example, what if the arguments contain something like the value of a user input text field? As the user types, more and more arguments will get cached, no?
Does that question make sense?
Again thank you so much for your assistance here, it's been a big help!
from reagent.
Yes, you have that all right. Full points :)
The only possible exception is the caching behaviour (and, as a consequence of that, when you should prefer function calls over component creation (i.e ()
vs. []
).
There's no risk of runaway cache growth, since there's just one value in the "cache" at any given time for a component. Caching works like this: when you re-render a component, React uses some heuristics to find the children that are "the same" (basically based on position and component type, i.e the first element in a hiccup form in Reagent's case, and based on the key
value if it is a javascript array/seq). When React has found a matching pair of new component instance and an old one, it calls shouldComponentUpdate
, with the old and new parameters to the component. Reagent's default implementation of shouldComponentUpdate
is basically to compare all the elements of the hiccup form vector using identical?
(except for maps, where all the keys/values are compared using identical?
).
If shouldComponentUpdate
returns true, render is called again (and any old values are thrown away), and the whole process is repeated for every child. Otherwise nothing else happens.
So the performance tradeoff between () and [] will depend on how costly the component function (i.e my-function
here) is. In this case it is extremely cheap, so using () will probably be a tiny bit faster (due to a couple fewer memory allocations). But I guess it would be very difficult to come up with a real world case where you could actually notice the difference – so it might be easier to just stick to [] all over the place by default.
A couple of more points, while I'm at it:
- As far as Reagent is concerned, function calls don't exist… It only cares that a component function returns a valid Hiccup form, but it doesn't know or care about how that is created (whether it is returned in-line, by function calls, macros, vector manipulation or anything else). The returned vector is just a value.
- As you may have noticed using the React Chrome plugin, Reagent actually creates two React components for
[:div "hello world"]
: one wrapper (that allows passing of ClojureScript data to children, and that sneaks in the defaultshouldComponentUpdate
), and one "native" Reactdiv
component. Outside the chrome plugin, I don't think it is even possible to tell that the wrapper is there, though (unless you mess with React internals).
Btw: very good questions! It is always very useful to have to explain the foundations of something, and in particular to discover what needs explaining and what not. And to re-discover the severe limitations of human telepathic capabilities – it is surprisingly easy to forget that people actually can't read your mind :)
from reagent.
Well, it is the behaviour I would expect :)
The difference between ()
and []
in this context is that the second form creates a new component instance, whereas calling with ()
just inlines whatever the component returns.
In all of your examples, keys will actually be generated, but at different depths. So when you have [foo1]
, you will get a "foo1" component without any key, but with a :li
child that has a key.
This, of course, only matters when you use a dynamic list of components, i.e a seq
. There the rule becomes that the top-level components contained in the seq
must have keys (since React doesn't look any deeper than the first level).
Does that make sense?
And yes, the "key requirement" in React is probably its least elegant part. But it does make diffing incredibly fast even for long lists of components…
from reagent.
Great, appreciate the clarifications. Managed to clear up the last of my missing :key
warnings now that I've got a better idea of the actual component hierarchy.
Enjoy the rest of your weekend & keep kicking ass - Reagent continues to be an absolute pleasure to use. Cheers! :-)
from reagent.
Thinking this through some more, I got to wondering how you were identifying fn equality for the purposes of component identity:
(defn my-component []
(let [state_ (reagent/atom nil)]
(fn []
(let [state' @state_
my-input
(fn [] [:input
{:value state'
:on-change (fn [ev]
(reset! state_
(-> ev .-currentTarget .-value)))}])]
[:div [my-input]]))))
So in cases like this where the component fn is actually defined within the render call, we lose fn identity and the [my-input]
component will actually get destroyed on every change.
I guess there's no way of avoiding that, right? I see that solutions here include using (my-input)
instead, or pulling the my-input
fn definition outside of the render scope to preserve its identity.
What would your usual approach here be? And I'm wondering, did you consider alternatives like maybe allowing fn metadata to convey identity like you've done with ^{:key _}
?
Otherwise I'm seeing two possibilities for a mental model here:
- Use
[my-component]
for top-level(def)
components only, otherwise prefer inlining. - Always define components outside of rendering scope, as with reactive atoms.
from reagent.
Yes, re-defining my-input
like that on every render is a bad idea in general... Not only will it force a re-render (since there's no way of knowing that it is the "same" function, as you say), it will also trigger a new call to React's createClass
on every render (since it is a brand new component class, not just a new component instance).
Allowing "identity by metadata" is an interesting idea. But that would require keeping some kind of registry of component classes, which might get hairy pretty quickly (and you'd have to be very careful not to get unlimited cache growth). I'd have to think some more about that...
Right now, I'm thinking that sticking to your two rules is better (both of them are valid, of course). In particular, using named functions as often as possible is probably a good idea in general.
Btw: the [:input ...]
component in your example would always re-render anyway, since :on-change
is always given a new value... Function equality is tricky in Clojure in general, and in ClojureScript in particular (since there are no vars).
But on the other hand, worrying too much about unnecessary re-renderings and such might be premature optimization. Unless your component function is particularly expensive (having thousands of children, or somesuch), most of the time you won't even notice the extra renderings. After all, plain React is quite fast, and it always re-renders by default.
from reagent.
Allowing "identity by metadata" is an interesting idea. But that would require keeping some kind of registry of component classes, which might get hairy pretty quickly (and you'd have to be very careful not to get unlimited cache growth). I'd have to think some more about that...
Okay, sure. Probably not worth the effort then, I think the current alternatives are quite reasonable.
Btw: the [:input ...] component in your example would always re-render anyway, since :on-change is always given a new value... Function equality is tricky in Clojure in general, and in ClojureScript in particular (since there are no vars).
Re-rendering is fine (and I'd agree with you that most of the time the performance difference won't be significant). The component destruction can have other undesirable effects though, like losing the input focus in this example. That's something I'd definitely want to try avoid.
Right now, I'm thinking that sticking to your two rules is better
Will do, thanks!
from reagent.
Re-rendering is fine (and I'd agree with you that most of the time the performance difference won't be significant). The component destruction can have other undesirable effects though, like losing the input focus in this example. That's something I'd definitely want to try avoid.
Oh, absolutely. I was just thinking about the :on-change
part. And yes, inputs have to be treated extra gently :)
from reagent.
I believe this is a related question. I was trying to figure out why my component got destroyed on every change, and this thread helped clarify- it's because I've lost function identity by defining the function within the render call.
;; child component
(defn list-element [id]
(with-meta
(fn [] [:li (:id @list)]) ;; @list is a map of ids to values
{:component-did-mount (fn [] (println "component mounted." id))}))
;; parent component
[:ul (for [[id _] @list] ^{:key id} [(list-element id)])]
Can you suggest an alternate implementation where the component doesn't get destroyed every change, yet still lets me access id
inside its lifecycle functions?
from reagent.
That was a very cool solution (even if it, as you say, will be quite inefficient)!
I would do something like this instead:
(def alist (atom {"id1" "foo", "id2" "bar"}))
(defn list-element [id]
[:li (@alist id)])
(def list-element'
(with-meta list-element
{:component-did-mount
(fn [this]
(let [[_ id] (reagent/argv this)]
(println "component mounted. " id)))}))
(defn parent-component []
[:ul (for [[id _] @alist]
^{:key id} [list-element' id])])
:component-did-mount
gets a reference to the mounted component (a.k.a this
), which is passed to reagent.core/argv
to get at the argument vector (i.e [list-element' id]
in this case). Definitely more boring than your solution, but it works :)
from reagent.
Darn, accidentally deleted my last comment..
After reading up on React, your solution now makes sense to me :)
The key for me was understanding the mounted component this
, along with reagent/props
and reagent/state
. Might make sense to call them out in the README documentation as I wasn't aware until I dug through the geometry example. I ended up modifying your solution above to use props:
(for [[id value] @alist] ^{:key id} [list-element` {:id id :value value}])
and calling reagent/props
in the lifecycle functions.
Thanks for making Reagent and being responsive on these threads. Very fun to use!
from reagent.
Related Issues (20)
- Bad react key warning when using false as key
- examples/material-ui not work HOT 1
- examples/material-ui not work HOT 1
- Docs on “Changed?” inaccurate? HOT 5
- React 18 does not live reload with Shadow-CLJS in non-trivial projects HOT 5
- Prop types
- `TypeError` when using function components with `r/create-class` with `dom.server/render-to-string`
- ReactDOM.render is no longer supported in React 18. Use createRoot instead. HOT 2
- shadow-cljs emits a warning when using with-let HOT 4
- Korean input broken on :input and :textarea HOT 1
- Update docs? HOT 1
- MUI ThemeProvider not working HOT 1
- @@ -62,6 +62,45 @@ class _Config { } } // eslint-disable-next-line valid-jsdoc /** * fetch problem title, level via solved.ac api * @see {@link https://solvedac.github.io/unofficial-documentation/#/operations/getProblemByIdArray} * @param {string[]} pids - list of problem id (up to 100) * @param { ({ problemId, titleKo, level }) => void } callback */ function fetchProblemsFromSolvedAc(pids, callback) { const query = encodeURIComponent(pids.join(',')); console.log(`https://solved.ac/api/v3/problem/lookup?problemIds=${query}`); fetch(`https://solved.ac/api/v3/problem/lookup?problemIds=${query}`) .then((res) => { console.log('fetchProblemsFromSolvedAc', res); return res; }) .then((res) => res.json()) .then(callback) .catch(() => callback(null)); }
- Readme says npm i or add deps to .edn but (in my case at least) needed both HOT 1
- Any interest in being able to replace `react/createElement` with a custom function? HOT 7
- The cursor disappears at the end of a controlled input on Chrome
- Reusable components following HTML semantics of Opional Attributes and Variadic Children HOT 3
- Controlled input loses cursor under ShadowRoot in React 18 HOT 3
- snake_case mentioned instead kebab-case in documentation of create-class function.
- Class name composition is harder than it needs to be
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from reagent.