I've come across this problem a couple of times now. As always, I just used a hacky solution as a one-off until it became a two-off and a three-off, so I decided to find out if there's a better way.
The Problem
Let's take an example. Imagine a Heading with an icon for an external link. However, this link is optional and not every heading needs to have one.
If you've been using React for a bit, you know how to show the icon only when we actually have a link provided
{link && (
<ArrowTopRightOnSquareIcon
className="aspect-square w-10 transition-all group-hover:-translate-y-1 group-hover:translate-x-1"
strokeWidth={2}
/>
)}
But here's the issue. You can conditionally render a complete element using the ternary / && operator. But here I need to wrap our heading in the <Link> tag only when we actually have a link
Hacky Solution
Just use a ternary right, easy
{link ? (
<a href={link}>
<h1>Title</h1>
</a>
) : (
<h1>Title</h1>
)}
That works, I guess, right? So that's what I was doing for a bit, but now let's say the heading has a whole bunch of styles, the heading has a span with the title and a span with an icon. And suddenly we have a whole bunch of duplicate code. We write code. We're severely allergic to duplicate code. So let's look at how we can make this better.
The Better Solution (Kinda writing this post, I would know)
Let's make a component - ConditionalWrapper.
import { type ReactNode } from "react"
const ConditionalWrapper = ({
condition,
wrapper,
children,
}: {
condition: boolean
wrapper: (children: ReactNode) => ReactNode
children: ReactNode
}) => (condition ? wrapper(children) : children)
export default ConditionalWrapper
Let's walk through this component. We take in some props. A condition we're looking for. If it passes, we wrap, if not we don't. A wrapper function if the condition passes. The wrapper takes the children as an input & returns the wrapper element with the children. Lastly, we need the children we can pass it down to both cases.
Let's see how I'm using it. Here's our heading/children.
<h1 className="group mb-4 flex w-fit items-center gap-4 text-4xl font-bold text-accent md:mb-8 md:text-5xl xl:text-6xl">
<span>{name}</span>
{link && (
<ArrowTopRightOnSquareIcon
className="aspect-square w-10 transition-all group-hover:-translate-y-1 group-hover:translate-x-1"
strokeWidth={2}
/>
)}
</h1>
And here's the Conditional Wrapper Component
<ConditionalWrapper
condition={Boolean(link)}
wrapper={(children) => <a href={link!}>{children}</a>}
>
<h1 className="group mb-4 flex w-fit items-center gap-4 text-4xl font-bold text-accent md:mb-8 md:text-5xl xl:text-6xl">
<span>{name}</span>
{link && (
<ArrowTopRightOnSquareIcon
className="aspect-square w-10 transition-all group-hover:-translate-y-1 group-hover:translate-x-1"
strokeWidth={2}
/>
)}
</h1>
</ConditionalWrapper>
Ignoring the h1 & styling, etc. We have a `condition` of `link` which is either an external link or null hence the converting to `Boolean()`. Then the wrapper arrow function which simply accepts children, wraps them in an `<a>` tag and spits it out in an implicit return. And finally, the Heading being our `children` prop.