React: Component VS Element as Prop
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 an 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:
component: basically function that return a JSX element
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