In React, you’ll sometimes need to allow users to pass along a dynamic tag name as a prop to a component, changing the rendered tag in the output HTML. Admittedly, this is a somewhat rare pattern in React. But it does exist in the wild. For example, you may have seen it if you’ve ever worked with the react-intl library for internationalization, where <FormattedMessage> can optionally render strings in the specified tag:

<FormattedMessage id="common.close" tagName="p" />

For the purposes of this demo, let’s say you want to create a reusable CenteredContainer wrapper component to center content on your page:

components/CenteredContainer/index.tsx
const CenteredContainer: FC = (props) => {
  const { className, children } = props;
  return <div className={classnames('centered', className)}>{children}</div>;
}

You could always render a <div> like we’re doing here, but that’s not a great idea. Not only does it pollute your DOM with an extra decorative <div>, but it also makes it more difficult for you to write semantic HTML markup that’s accessible and easy to parse at a glance. Plus, there’s no reason why a centered container should always be a <div>.

One potential solution is to just create a reusable, globally scoped .centered class name and style it accordingly. Instead of using a component, simply slap that class name on any component that needs it. This certainly works, but it also has its downsides. Namely, if you’re working with Next.js or any React project that enforces CSS modules or scoped styling, this is somewhat of an anti-pattern since you’re introducing global styling that may or may not conflict with class names elsewhere in the DOM.

A better solution is to take advantage of TypeScript’s intellisense and pass a dynamic tag name as a prop to change the rendered tag:

components/CenteredContainer/index.tsx
import { FC } from 'react';
import classnames from 'classnames';

export interface CenteredContainerProps
  extends React.HTMLAttributes<HTMLOrSVGElement> {
  tagName?: keyof JSX.IntrinsicElements;
  className?: string;
}

const CenteredContainer: FC<CenteredContainerProps> = (props) => {
  const { className, children, tagName } = props;
  const Tag = tagName as keyof JSX.IntrinsicElements;
  return (
    <Tag className={classnames('centered', className)}>
      {children}
    </Tag>
  );
};

CenteredContainer.defaultProps = {
  tagName: 'div',
};

export default CenteredContainer;

Note: If you’re not using the classnames package, I highly recommend that you install it. It’s a nice little utility that generates class strings and takes care of undefined/nullish values for you.

There are two things worth noting here.

First, React expects element types to start with a capital letter. Lowercase element names are reserved for built-in tags like <div>, for example. But we want to follow the convention of using lowercase names for props. To get around this, we create a new, uppercase variable Tag that gets a copy of tagName:

const Tag = tagName as keyof JSX.IntrinsicElements;

Second, you may be wondering why we’re using a type assertion here (as well as in the interface):

as keyof JSX.IntrinsicElements

That addresses the following error, letting TypeScript know that our Tag variable does in fact resolve to one of the built-in (intrinsic) callable element types, like <div>, <button>, and so on:

"JSX element type 'Tag' does not have any construct or call signatures"

Additionally, notice that our CenteredContainer component renders a <div> by default:

CenteredContainer.defaultProps = {
  tagName: 'div',
};

But you can override this by passing in a custom tagName:

components/Navbar/index.tsx
import CenteredContainer from '@components/CenteredContainer';
import { FC } from 'react';

const MyComponent: FC = (props) => {
  return (<CenteredContainer tagName="nav">
    {props.children}
  </CenteredContainer>);
}

Since we’ve specified that tagName is keyof JSX.IntrinsicElements, we’ll get auto-complete intellisense whenever we try to set this prop:

An example of using the CenteredContainer component and passing in a concrete tagName. VS Code's intellisense shows an auto-complete dropdown for you as you type.

Note that render props are yet another alternative pattern, but they’re not always needed. You typically only need to use render props if the element being rendered depends on some state. Here, we’re just telling the component what to render by passing in a plain string. The syntax is shorter and easier to read.

That said, you may want to use render props if you want to pass any other dynamic props to your component, aside from a class and some children. Otherwise, you’ll need to spread those remaining props in your component and extend the appropriate interfaces.

That’s it! I hope you found this helpful.

Attributions

Thumbnail photo by Paolo Chiabrando via Unsplash.

💬 Comments

Post comment
Loading...