In React, you’ll sometimes want to allow users to pass in a dynamic tag name as a prop to a component. While this is a somewhat rare pattern, you may still come across it in the wild. For example, you may have seen it if you’ve ever worked with the react-intl
library for internationalized strings, where the <FormattedMessage>
component accepts an optional tagName
prop:
<FormattedMessage id="common.close" tagName="p" />
To keep this demo simple, let’s say we want to create a reusable CenteredContainer
component to encapsulate some CSS or other logic (I won’t show any of that here):
import React, { FC } from 'react';
interface CenteredContainerProps {
className?: string;
}
const CenteredContainer: FC<CenteredContainerProps> = (props) => <div {...props} />;
You could always render a <div>
like we’re doing here, but that’s not a great idea. It pollutes your DOM with an extra decorative <div>
, making it difficult to write semantic HTML markup. Plus, there’s no reason why a centered container should always be a <div>
. This may even be invalid HTML depending on where you intend to use the container.
Fortunately, we can take advantage of TypeScript’s intellisense and pass a dynamic tag name as a prop to change the rendered tag:
import React, { FC } from 'react';
interface CenteredContainerProps extends React.HTMLAttributes<HTMLOrSVGElement> {
tagName?: keyof JSX.IntrinsicElements;
className?: string;
}
const CenteredContainer: FC<CenteredContainerProps> = ({ tagName, ...otherProps }) => {
const Tag = tagName as keyof JSX.IntrinsicElements;
return <Tag {...otherProps} />;
};
CenteredContainer.defaultProps = {
tagName: 'div',
};
export default CenteredContainer;
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
:
import CenteredContainer from 'components/CenteredContainer';
import React, { FC } from 'react';
const MyComponent: FC = ({ children }) => {
return <CenteredContainer tagName="nav">{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:
In some cases, you may want to instead use render props, but they’re not always needed—sometimes, all you really want is to be able to specify a tag name as a string. 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.
And that’s all there is to it! I hope you found this mini-tutorial helpful.
Attributions
The photo used in this post’s social media preview was taken by Paolo Chiabrando and uploaded to Unsplash.
💬 Comments
Post comment