admin管理员组

文章数量:1128017

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?

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?

Share Improve this question edited Jul 10, 2022 at 11:15 cobrexus 4,7885 gold badges23 silver badges49 bronze badges asked Nov 2, 2015 at 6:26 Eranga KapukotuwaEranga Kapukotuwa 4,9325 gold badges26 silver badges30 bronze badges 0
Add a comment  | 

10 Answers 10

Reset to default 589

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>

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>

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 invoke React.createElement() directly if you are using JSX. See React Without JSX to learn more.

All the other answers are working fine but I would add some extra, because by doing this:

  1. It is a bit safer. Even if your type-checking is failing you still return a proper component.
  2. It is more declarative. Anybody by looking at this component can see what it could return.
  3. Its is more flexible for example instead of 'h1', 'h2', ... for type of your Heading you can have some other abstract concepts 'sm', 'lg' or 'primary', 'secondary'

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>

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.

See Example

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;

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);
//for Typescript
interface ComponentProps {
    containerTag: keyof JSX.IntrinsicElements;
}

export const Component = ({ containerTag: CustomTag }: ComponentProps) => {
    return <CustomTag>Hello</CustomTag>;
}

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>
  )
}

React + TypeScript

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.

本文标签: javascriptDynamic tag name in React JSXStack Overflow