# Polymorphic components in React

Every now and then, I encounter a situation where I need a link that looks like a button. Or a button that looks like a link. Isn't it a perfect use case for a polymorphic component? In this article, I'll argue **why polymorphic components should be avoided**. I'll also present a pattern that can be used instead.

## What are polymorphic components?

You've probably heard of them. Seen them. Used them. Maybe even tried building one. Simplifying, **polymorphic components render as different elements based on props passed to them**. A typical example is `LinkButton` that takes an `as` prop and can either be a `button` or `a`:

```ts
<LinkButton
	as="a"
	href="/"
/>
 
<LinkButton
	as="button"
	onClick={(e) => {
		console.log(e);
	}}
/>
```

Mind how the `as` prop influences other props as well. In this case, when passing `as="a"`, you'll also need to add `href`. And when passing `as="button"`, `href` will be forbidden. Looks nice.

## Polymorphic components in TypeScript

Implementing polymorphic components in TypeScript requires a wee bit of trickery. You can read all about it in this excellent piece by Matt Pocock: [Polymorphic Link/Button Components in React & TypeScript](https://www.totaltypescript.com/polymorphic-link-button-components-in-react-and-typescript). Doesn't look so bad! But…

## Accidental and true duplication

In "Clean Architecture" by Robert C. Martin, the author mentions a concept that deeply resonated with me. **There are two kinds of duplications: true (bad) duplication and false (accidental) duplication.**

> True duplication is when the same code or logic is repeated in multiple places.
> 
> Accidental duplication is when the same code or logic is repeated in multiple places.

Just by looking at the code *in situ*, **it's impossible to say which kind of duplication we're dealing with**. That's why it's so tricky. Only when the codebase starts evolving will we see that accidentally similar pieces of code are changing in a different manner and for various reasons. In contrast, **truly duplicated code always changes in the exact same way at the same time**. That's why it's so important not to avoid duplication at all costs. I suggest waiting and observing which pieces of code will benefit from deduplicating.

What does it have to do with anything? My experience tells me that **polymorphic components are mostly used to deduplicate accidentally similar pieces of code**.

## Diverging polymorphic components

Consider the `LinkButton` example above. What happens when we need to add `disabled` state support for both `a` and `button`? **The underlying implementations will differ significantly**. In the case of a button, it'll be as simple as adding a `disabled` attribute to the HTML element. However, for `a` to be "disabled", we'll need to add an `onClick` handler whose sole responsibility is to `preventDefault`. Moreover, the `aria-disabled` attribute should be added as well. That forces us to write dirty conditional code. Not only will it cause trouble with maintenance in the future, but it'll be instantly annoying when it comes to getting the types right. Try it for yourself!

Even worse, we're mixing responsibilities! **There's no clear separation between the UI and logic**. We can't simply reuse `LinkButton`'s looks for other elements without modifying it.

**What happens when we use the same pattern for more complex cases** if it's a pain in such a simple scenario? Think of `<Container as=…>` or `<Card as=…>` – commonly found in different codebases. Yikes.

## Enter `asChild`

The `asChild` pattern solves all the mentioned problems. It clearly separates responsibilities. Components built with `asChild` in mind are **easily reusable for their looks regardless of the component's role**. Let's see it in action:

```ts
<UIButton asChild>
	<a href="/">Go back home</a>
</UIButton>
```

```ts
<UIButton asChild>
	<button onClick={…}>Do something</button>
</UIButton>
```

Smooth, right?

The `UIButton` component is **completely agnostic of the element inside** – an `a`, `button`, `div` or any other. Moreover, no additional HTML tags are rendered.

## `asChild` and `Slot` Implemented

Often, the `asChild` is optional. When you omit it, a default element is rendered instead of relying on the child. See Radix, for example. However, it stems from my experience that **it's a good practice to make the child element required**. I **never render the default element** and always expect the developers to use `children`. In this case, the `asChild` prop is not really required. It's just a pattern. A clear way of indicating that a given component needs to be used in a certain way and doesn't render anything on its own.

Either way, the true magic happens inside the `Slot` component:

```ts
import clsx from "clsx";
import { isValidElement, cloneElement, Children, type HTMLAttributes, type ReactElement } from "react";

export const Slot = ({
	children,
	...props
}: HTMLAttributes<HTMLElement> & {
	children: ReactElement;
}) => {
	if (isValidElement<HTMLAttributes<HTMLElement>>(children)) {
		return cloneElement(children, {
			...props,
			...children.props,
			className: clsx(children.props.className, props.className),
		});
	}
	throw new TypeError(`Single element child is required in Slot`);
};
```

What's going on here? Let's go through the code step by step. We declare a `Slot` component that takes all props that any element could take. Additionally, we require `children` of type `ReactElement` – meaning that **exactly one element needs to be passed to it**.

Then, we do a runtime validation of whether the provided `children` is, in fact, a valid React element. If it is, we clone that element, merge props passed to Slot and the element, and return it. We also take extra care of the `className` using the `clsx` library. You might want to use `tailwind-merge` instead if that's your jam.

Then, the `Slot` is reused in different `UI…` components in the following manner:

```ts
import { clsx } from "clsx";
import { type ReactElement, type ButtonHTMLAttributes } from "react";
import { Slot } from "./Slot";

interface UIButtonProps
	extends ButtonHTMLAttributes<HTMLButtonElement> {
	asChild: true;
	children: ReactElement;
}

export const UIButton = ({
	asChild: _,
	...props
}: UIButtonProps) => {
	return (
		<Slot
			{...props}
			className={clsx(
				"your classes to make it look nice",
				props.className,
			)}
		/>
	);
};
```

We ignore the `asChild` and pass all remaining props to `Slot`. We also add classes responsible for how the element is supposed to look and merge them with the parent class names. And this is it!

## Summary

**No conditional logic, straightforward separation of concerns, easy maintenance.** There's now only a single reason to modify the `UI…` components: when we need to change how they look.

The added benefit is how simple it has become to build a Storybook with such components. **No need to mock any logic or providers.** Just use the `UI…` components with bare-bone elements, and you're done!

I can't imagine building interfaces without the `asChild` pattern anymore. Do you like it? Let me know in the comments.
