reactjs - Skriva en dynamisk tagg i React med TypeScript?


Hur skriver jag en dynamisk tagg i React med TypeScript? Givet denna kod:

interface CompProps {
  tag: string;
}

const MyComponent: React.FunctionComponent<CompProps> = ({
  tag = "div",
  children
}) => {
  const Wrapper = tag;

  return <Wrapper>{children}</Wrapper>;
};

Jag får detta felmeddelande:

Typen '{ children: ReactNode; }' har inga egenskaper gemensamma med typen 'IntrinsicAttributes'. ts(2559)

Det verkar som att jag måste lägga till rätt typer men jag kan inte lista ut vilka.

Svar #1

Du kan skicka in en string som ett taggnamn och använda det som du har gjort, men du behöver skriva det korrekt för att typkontrollen ska fungera. tag bör vara en nyckel i JSX.IntrinsicElements.

interface CompProps {
  tag: keyof JSX.IntrinsicElements;
}

const MyComponent: React.FunctionComponent<CompProps & React.HTMLAttributes<HTMLOrSVGElement>> = ({
  tag: Wrapper = "div",
  children,
  ...rest
}) => {
  return <Wrapper {...rest}>{children}</Wrapper>;
};

Lekplatslänk

Svar #2

Använda en typdefinition för alla HTML-element

För att tillåta alla HTML-element att användas som din tagg kan du använda nycklarna i IntrinsicElements-gränssnittet som definieras i JSX-namnrymden. IntrinsicElements verkar innehålla en mappning av HTML-elementtaggar till deras respektive attribut (inkluderar elementspecifika attribut). För att använda dessa nycklar kan vi göra följande:

interface Props {
  tag?: keyof JSX.IntrinsicElements
}

Vad händer om jag vill tillåta att React-komponenter används som tagg?

React definierar två gränssnitt: ComponentClass och FunctionComponent. React definierar också en union av dessa två gränssnitt som låter dig specificera vilken React-komponent som helst: ComponentType. Vi kan skapa en union av detta och vår senaste definition för att tillåta både komponenter och HTML-taggar.

import { ComponentType } from 'react';

interface Props {
  tag?: ComponentType | keyof JSX.IntrinsicElements;
}

Nåväl, nu har jag en tagg, vad sägs om HTML-attribut?

Om du vill tillåta alla andra HTML-attribut kan du antingen utöka React.HTMLAttributes<Element> för att få alla delade HTML-attribut (inga element-specifika) eller så kan du införa en generisk och använda JSX.IntrinsicElements.

Det andra alternativet är mer komplext och kommer med några förbehåll. Du måste använda type istället för interface för att utöka/intersektera dina Props och de specifika attribut som definieras på en nyckel i JSX.IntrinsicElements. Du kommer också att behöva använda generics på din funktion så att du kan skicka dem till din Props-typ, vilket betyder att du inte längre kan använda React.FunctionComponent<Props> eftersom det sker innan åtkomst till några generics. Det betyder att du vill lägga till children i din Props-definition.

Det var många ord som jag tror förklaras bättre med detta exempel:

// Define our Props type to allow the specifying of a Tag for HTML attributes
// Also define children as React does with React.ReactNode
type Props<Tag extends keyof JSX.IntrinsicElements> = {
  tag?: ComponentType | keyof JSX.IntrinsicElements;
  children?: ReactNode;
} & JSX.IntrinsicElements[Tag];

// Define our generic (Tag) again here and give it our default value
// Don't forget to specify the type Props<Tag> at the end of your function's arguments
// Then we can spread all props to the tag/Wrapper
function MyComponent<Tag extends keyof JSX.IntrinsicElements = 'div'>({ tag: Wrapper = 'div', ...props }: Props<Tag>) {
  return <Wrapper {...props} />;
}

// Example usage, noValidate is typed as
// (JSX attribute) React.FormHTMLAttributes<HTMLFormElement>.noValidate?: boolean | undefined
<MyComponent<'form'> tag="form" noValidate>
  {/* My Form Stuff */}
</MyComponent>;

// You don't need to specify 'div' since it is the default
<MyComponent id="page">
  <p>Just a paragraph inside of a regular div</p>
</MyComponent>;

Relaterade artiklar