In this article we will look into how to create a component that accepts another component/element as prop effectively.
Why do we need it?
We already have children prop, so why do we need this?
Imagine we want to create a button component, normally the child is text. And now we need another child for icon, this is where component/element as prop is useful, it allows us to have a child in separated parents
There are 2 ways, passing as component and passing as element.
Terminology:
1) component: function that return a JSX element
2) element: JSX element
We will have a clear answer on which pattern we should use in the end.
1) Passing as component
const A = ({ component }: { component: () => JSX.Element }) => {
const Component = component;
return <Component />;
};
const B = () => {
return <div>as component</div>;
};
export default function App() {
return <A component={B} />;
}
so far so good, but what if you need to pass some state to B?
import { useState } from "react";
const A = ({ component }: { component: () => JSX.Element }) => {
const Component = component;
return <Component />;
};
const B = ({ count }: { count: number }) => {
return <div>{`as component with state:${count}`}</div>;
};
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<A component={() => <B count={count} />} />
<button
onClick={() => {
setCount((count) => count + 1);
}}
>
increase the count
</button>
</>
);
}
that seems to do it, it is working.
however please take a closer look at () => <B count={count} />
, something is not right, can you guess what is it?
now try the same code again, but this time we add useEffect to B to track the mount and unmount event
import { useEffect, useState } from "react";
const A = ({ component }: { component: () => JSX.Element }) => {
const Component = component;
return <Component />;
};
const B = ({ count }: { count: number }) => {
useEffect(() => {
console.log("mounted");
return () => {
console.log("unmounted");
};
}, []);
return <div>{`as component with state:${count}`}</div>;
};
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<A component={() => <B count={count} />} />
<button
onClick={() => {
setCount((count) => count + 1);
}}
>
increase the count
</button>
</>
);
}
now open the console and increase the count, this is what you will see:
this is because for every rerender(function rerun), () => <B count={count} />
return a new reference because it is a anonymous function, so React think it is a different component and remount it.
can we solve this? The solution is simple, just pass it as element instead
2) Passing as Element
import { useEffect, useState } from "react";
const A = ({ element }: { element: JSX.Element }) => {
return element;
};
const B = ({ count }: { count: number }) => {
useEffect(() => {
console.log("mounted");
return () => {
console.log("unmounted");
};
}, []);
return <div>{`as component with state:${count}`}</div>;
};
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<A element={<B count={count} />} />
<button
onClick={() => {
setCount((count) => count + 1);
}}
>
increase the count
</button>
</>
);
}
as you can see, the component is only mounted twice**
That is all, passing as element is the pattern we should use.
**React 18 mount event run twice in dev environment, source
Top comments (4)
yes it is, but what I mean is normally people use the child for the text element (see more explanation in below)
No, you missed the point
I mean, of course, you can do that, that is because I use a simple example, but this is not the purpose I created this post
now imagine you have an App component, it has 3 sections, header, content, and footer
now imagine I already defined the body, I want another dev to define the header and footer, so obviously you cannot do this in one child (or one big child) because the header and footer should not be in the same component
This is when an element as a prop comes to play, simply create a header and footer props and let another dev define them.
Technically you can add a lot of CSS to achieve the same effect, but that is not really a good way to do things.
Again this is just an example and probably not a good one because practically speaking nobody defines the body before the header
But what am I saying is the one one big children is not useful if the component has proper layout for their children, another simple example is left icon, right icon, and loader for button.
Normally the component creator already positioned those element well, you just need to fill the element prop without worrying about the position and also the conditional logic for the loader.
many UI libraries use this kind of pattern for button, example is material ui, ant design, mantine and etc etc: child for text, element props for left, right icon and loader.
This pattern is very useful, convenient and common
again, I am not specifically talking about button and icon, I am talking about component vs element as prop in general, and why people choose element prop over component prop
it is not
React is unopinionated, it doesn't care how user practise it, the reason is something else
children here is normally a text refer to libraries like material UI
did you even bother to click the links I provided?
material ui
ant design
mantine
I am talking about usage, normally people use it as button text, just like the plain
<button>text</button>
!those child become button text!
I said normally, not only, I am not saying it accepts only string!
so yeah, you misunderstood
and I perfectly aware that the child can accept any react element, you could even try to fit the whole app in the button child
for god sake, people are not stupid
please understand the context
what I mean here is you you need to wrap them in one big child(eg in a fragment), except array, so yeah I agree it has some inaccuracy there
please no strawman argument here, thanks, that is not totally what I mean
stop argument like this, because it is very difficult to explain to you what is wrong with your logic
You are expecting people to express themselves in 100% accuracy, this is unrealistic, even I cant express myself with 100% accuracy in my native tongue
even with 100% accuracy, the receiver may still understand it in some other way
write cannot express themself with 100% accuracy, reader cannot understand it with 100% accuracy, it is just normal because we as human are not perfect.
I understand that people may not get the point when they first read the post, which is why I try to clarify what this post is about in my first reply to you, but you ignore it
if you ignore it, then that is your problem, not mine
inaccuracy in communication is something that we need to tolerate, hence understanding the whole context is important, not paying excessive attention to wording!
if you insist to talk pedantically, then you are looking for troubles
next time remember to understand the context when you read the post!
no, you still don't understand what is the purpose of this post
and as I said my example is just an example to explain concept, not really to build some component
example that you are showing is working and nothing is wrong with it, but that is because it is simple
in a complex UI, thing is much more complicated than any of the example shown in here, the creator already did the layout for us, the position of the internal element, responsiveness, flex etc etc and also deal with conditional logic for stuff like loading
an example of a complex AppShell , it simply make more sense to use element prop than just using child here
even simple components also benefit from this, examples are the buttons
and there are two ways to achieve this, component vs element, and the purpose of this post is to demonstrate why element is better than component, due to the remount issue (this issue is quite well known in the old version of React Router, where RR provided all three options, child, element and component as prop)
which is why majority of the UI libraries use element as prop pattern
I did not claim that I understand ReactNode, I simply telling you what I understood, I accept that my understanding may be flawed, you are free to point it out and I thank you for that example, I learned something
that was indeed a strawman argument, I simply point it out what is wrong with your argument, just like you trying to point out what is wrong with mine
the children part you are right, but you totally misinterpreted
normally the child is text.
it is impossible to accept a component only as a single child, as you already corrected, hence
that's a component with a poor API
is not an existing issuecomponent that use element prop is also capable to accepts multiple child
doesn't imply it is bad either, but since majority component libraries using such pattern and you did not provide a good counter point against complex component, internal layout and logic handling, it is just normal for me to favor such approach, especially if I want try to create a ui libraries or a complex component
basically those authors approach are more credible than yours
English is my fourth, but I couldnt care less, nobody can talk and understand with 100% accuracy, no matter his 1st, 2nd and 3rd, that is the point
stop implying I am such person, I am pissed because you are trying to blame me on why you derailed with flawed analogy
Some comments have been hidden by the post's author - find out more