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
forwardRefinnpakket 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
