reactjs - Krav om et barn som aksepterer en ref-attributt i React + Typescript


Ved å bruke React + Typescript, vil jeg lage en children-prop som kun aksepterer ett barn som forstår og aksepterer en ref-attributt. I utgangspunktet bør typen for mine children akseptere:

  • en instans av en React-klassekomponent
  • et standard React HTML-element (f.eks. <button>, <div>)
  • en forwardRef innpakket funksjonskomponent

Det bør ikke akseptere:

  • en vanlig funksjonskomponent
  • en tekstnode, som en streng eller et tall

Målet mitt med denne children-typen er å injisere en ref i barnet ved å bruke React.cloneElement med en viss grad av sikkerhet for at ref ikke blir fullstendig ignorert.

Jeg har prøvd

import React from "react";

type Props = {
    // EDIT: This is how I originally typed `children` to
    // get a decent compile time check that returns errors
    // if the children is a text node or multiple elements.
    // The same effect can be achieved with the `ReactElement` type.
    // children: React.FunctionComponentElement<React.RefAttributes<HTMLElement>>;
    children: React.ReactElement;
};

const MyComponent = (props: Props) => {
    const childRef = React.useRef(null);

    const child = React.Children.only(props.children);
    if (!React.isValidElement(child)) {
        return props.children;
    }
    return React.cloneElement(child, {ref: childRef});
};

Dette kompilerer, og MyComponent godtar ikke tekstnoder som barn, men det godtar funksjonskomponenter uten forwardRef uten klage.

// This fails with an error that a text node is not a valid child.
<MyComponent>hello</MyComponent>

// This compiles without issue, but should fail because MyText is not a `forwardRef` component.
const MyText = () => <span>hello</span>;
<MyComponent><MyText/></MyComponent>

Jeg har også prøvd noen andre typer, men dette føles som det nærmeste jeg har kommet.

Er det mulig å lage et typisk krav som dette med Typescript? Hvis ja, hvordan kan det gjøres?

Svar #1

Nei.

Det finnes ingen måte å håndheve at en instans av en React-komponent bruker React.forwardRef(). Det er fordi React ikke bevarer typen til den opprinnelige funksjonen eller klassen en komponentinstans ble opprettet med. En instans av en React-komponent i JSX vil alltid være JSX.Element, React.FunctionComponentElement<...>, eller React.CElement<...>.

Eksempel :

const MyForwardedRefComponent = React.forwardRef<HTMLDivElement>((props, ref) => {
  return <div ref={ref}>{props.children}</div>;
});

// JSX.Element
const myInstance = <MyForwardedRefComponent />;

// React.FunctionComponentElement<{}>
const myOtherInstance = React.createElement(MyForwardedRefComponent);

Imidlertid kan du tvunget at en React-komponent kun aksepterer ett enkelt barneelement, og TypeScript vil advare deg i henhold til dette. Dette er omtrent så godt som du kan håpe på i ditt tilfelle.

Eksempel :

const SingleChildOnly: React.FC<{ children: React.ReactElement }> = ({
  children,
}) => {
  const ref = React.useRef(null);
  const child = React.Children.only(children);

  return React.cloneElement(child, { ref });
};

const thisIsOkay = (
  <SingleChildOnly>
    <div>hello</div>
  </SingleChildOnly>
);

// error
const thisIsNotOkay = (
  <SingleChildOnly>
    <div>hello</div>
    <div>hello there</div>
  </SingleChildOnly>
);

Svar #2

Jeg kunne heller ikke finne en TypeScript-løsning for dette.

Det jeg ønsket å gjøre var å lage en Tooltip-komponent for å omslutte andre komponenter med.

Det beste jeg kan komme opp med så langt, er å skrive SingleChildOnly-komponenten som en tilpasset hook som vil returnere: en referanse å sette på barnet, eventuelle props å legge til barnet og elementet du vil rendere.

På denne måten kan du tvinge barnekomponenten til å kunne akseptere en ref.

Ulempen er at du må skrive mye mer kode når du bruker SingleChildOnly-komponenten.

Jeg tenkte på å lage TextWithTooltip ButtonWithTooltip basis komponenter som kun omslutter en enkel HTML span eller knapp.

Vet ikke om det er anvendelig for din brukstilfelle

Relaterte artikler