React: Component VS Element as Prop

Acid Coder
3 min readJun 27, 2022

--

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

--

--

Acid Coder

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