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} />;
}

codesandbox

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>
</>
);
}

codesandbox

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>
</>
);
}

codesandbox

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>
</>
);
}

codesandbox

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

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Acid Coder

Typescript Zombie. Youtube Pikachu On Acid. (Unrelated to programming but by watching it you become a good developer overnight)