Reputation: 4952
I am trying to write a React component for HTML heading tags (h1
, h2
, h3
, etc.), where the heading level is specified via a prop.
I tried to do it like this:
<h{this.props.level}>Hello</h{this.props.level}>
And I expected output like:
<h1>Hello</h1>
But this is not working.
Is there any way to do this?
Upvotes: 341
Views: 179302
Reputation: 1268
This implementation defines a concise type-safe composable method for the OP's problem. It defines a props type that will encompass the properties of any valid heading tag (h1-h6) plus any valid React component (e.g. className
, children
, etc).
It also implements type safety on the 'level' property by limiting the value to be between 1 and 6.
It also addresses the "union" error from previous answers.
The HeadingWithRef
example takes it a step further and allows the parent to access the primitive DOM's properties, in case you need to do something dynamic with it or read the primitive element's property values.
//Define the heading properties type
interface HeadingProps
extends React.ComponentProps<"h1" | "h2" | "h3" | "h4" | "h5" | "h6"> {
level: 1 | 2 | 3 | 4 | 5 | 6;
}
//Define the component
const Heading: React.FC<HeadingProps> = ({ level, ...props }) => {
const Tag: keyof JSX.IntrinsicElements = `h${level}`;
return <Tag {...props} />;
};
//If you need ref forwarding
const HeadingWithRef = React.forwardRef<HTMLHeadingElement, HeadingProps>(
({ level, ...props }, ref) => {
const Tag: keyof JSX.IntrinsicElements = `h${level}`;
return <Tag ref={ref} {...props} />;
}
);
HeadingWithRef.displayName = "HeadingWithRef";
Usage Example
//This will render an <h2> tag
export default function DemoPage() {
return (
<>
<header>
<Heading level={2}>Heading</Heading>
</header>
</>
);
}
Disclaimer: I'm not a developer. I came up with this solution while working on a personal project and it seems to also address the OP's issue via TypeScript so I thought I'd share it.
Upvotes: 0
Reputation: 5279
If you're using TypeScript, you'll have seen an error like this:
Type '{ children: string; }' has no properties in common with type 'IntrinsicAttributes'.ts(2559)
TypeScript does not know that CustomTag
is a valid HTML tag name and throws an unhelpful error.
To fix, cast CustomTag
as keyof JSX.IntrinsicElements
!
// var name must start with a capital letter
const CustomTag = `h${this.props.level}` as keyof JSX.IntrinsicElements;
// or to let TypeScript check if the tag is valid
// const CustomTag : keyof JSX.IntrinsicElements = `h${this.props.level}`;
<CustomTag>Hello</CustomTag>
Upvotes: 194
Reputation: 41
//for Typescript
interface ComponentProps {
containerTag: keyof JSX.IntrinsicElements;
}
export const Component = ({ containerTag: CustomTag }: ComponentProps) => {
return <CustomTag>Hello</CustomTag>;
}
Upvotes: 3
Reputation: 8091
In the instance of dynamic headings (h1, h2...), a component could return React.createElement
(mentioned above by Felix) like so.
const Heading = ({level, children, ...props}) => {
return React.createElement('h'.concat(level), props , children)
}
For composability, both props and children are passed.
Upvotes: 13
Reputation: 424
This is how I set it up for my project.
TypographyType.ts
import { HTMLAttributes } from 'react';
export type TagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span';
export type HeadingType = HTMLAttributes<HTMLHeadingElement>;
export type ParagraphType = HTMLAttributes<HTMLParagraphElement>;
export type SpanType = HTMLAttributes<HTMLSpanElement>;
export type TypographyProps = (HeadingType | ParagraphType | SpanType) & {
variant?:
| 'h1'
| 'h2'
| 'h3'
| 'h4'
| 'h5'
| 'h6'
| 'body1'
| 'body2'
| 'subtitle1'
| 'subtitle2'
| 'caption'
| 'overline'
| 'button';
};
Typography.tsx
import { FC } from 'react';
import cn from 'classnames';
import { typography } from '@/theme';
import { TagType, TypographyProps } from './TypographyType';
const headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
const paragraphs = ['body1', 'body2', 'subtitle1', 'subtitle2'];
const spans = ['button', 'caption', 'overline'];
const Typography: FC<TypographyProps> = ({
children,
variant = 'body1',
className,
...props
}) => {
const { variants } = typography;
const Tag = cn({
[`${variant}`]: headings.includes(variant),
[`p`]: paragraphs.includes(variant),
[`span`]: spans.includes(variant)
}) as TagType;
return (
<Tag
{...props}
className={cn(
{
[`${variants[variant]}`]: variant,
},
className
)}
>
{children}
</Tag>
);
};
export default Typography;
Upvotes: 6
Reputation: 6691
Generalising robstarbuck's answer you can create a completely dynamic tag component like this:
const Tag = ({ tagName, children, ...props }) => (
React.createElement(tagName, props , children)
)
which you can use like:
const App = ({ myTagName = 'h1' }) => {
return (
<Tag tagName={myTagName} className="foo">
Hello Tag!
</Tag>
)
}
Upvotes: 2
Reputation: 816302
For completeness, if you want to use a dynamic name, you can also directly call React.createElement
instead of using JSX:
React.createElement(`h${this.props.level}`, null, 'Hello')
This avoids having to create a new variable or component.
With props:
React.createElement(
`h${this.props.level}`,
{
foo: 'bar',
},
'Hello'
)
From the docs:
Create and return a new React element of the given type. The type argument can be either a tag name string (such as
'div'
or'span'
), or a React component type (a class or a function).Code written with JSX will be converted to use
React.createElement()
. You will not typically invokeReact.createElement()
directly if you are using JSX. See React Without JSX to learn more.
Upvotes: 55
Reputation: 254886
No way to do that in-place, just put it in a variable (with first letter capitalised):
const CustomTag = `h${this.props.level}`;
<CustomTag>Hello</CustomTag>
Upvotes: 592
Reputation: 371
You can give this a try. I implement like this.
import { memo, ReactNode } from "react";
import cx from "classnames";
import classes from "./Title.module.scss";
export interface TitleProps {
children?: ReactNode;
className?: string;
text?: string;
variant: Sizes;
}
type Sizes = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
const Title = ({
className,
variant = "h1",
text,
children,
}: TitleProps): JSX.Element => {
const Tag = `${variant}` as keyof JSX.IntrinsicElements;
return (
<Tag
className={cx(`${classes.title} ${classes[variant]}`, {
[`${className}`]: className,
})}
>
{text || children}
</Tag>
);
};
export default memo(Title);
Upvotes: 3
Reputation: 5334
All the other answers are working fine but I would add some extra, because by doing this:
The Heading component:
import React from 'react';
const elements = {
h1: 'h1',
h2: 'h2',
h3: 'h3',
h4: 'h4',
h5: 'h5',
h6: 'h6',
};
function Heading({ type, children, ...props }) {
return React.createElement(
elements[type] || elements.h1,
props,
children
);
}
Heading.defaultProps = {
type: 'h1',
};
export default Heading;
Which you can use it like
<Heading type="h1">Some Heading</Heading>
or you can have a different abstract concept, for example you can define a size props like:
import React from 'react';
const elements = {
xl: 'h1',
lg: 'h2',
rg: 'h3',
sm: 'h4',
xs: 'h5',
xxs: 'h6',
};
function Heading({ size, children }) {
return React.createElement(
elements[size] || elements.rg,
props,
children
);
}
Heading.defaultProps = {
size: 'rg',
};
export default Heading;
Which you can use it like
<Heading size="sm">Some Heading</Heading>
Upvotes: 26