danwoz
danwoz

Reputation: 85

Requiring a child that accepts a ref attribute in React + Typescript

Using React + Typescript, I'd like to create a children prop that only accepts a single child that accepts a understands and accepts a ref attribute. Basically, my children type should accept:

It should not accept:

My goal with this children type is to inject a ref into the child using React.cloneElement with some level of certainty that ref will not go completely ignored.

I have tried

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

This compiles, and MyComponent not accept text nodes as children, but it does accept non forwardRef function components without complaint.

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

I've also tried some other types, but this feels like the closest I've gotten.

Is creating a type requirement like this possible with Typescript? If so, how can it be done?

Upvotes: 5

Views: 3501

Answers (2)

Alissa
Alissa

Reputation: 1984

I also couldn't find a typescript solution for this.

What I wanted to do is create a Tooltip component to wrap other components with.

The best I can come up with so far, is writing the SingleChildOnly component as a custom hook which will return: a reference to put on the child, any props to add to the child and the Element you want to render.

This way you can force the child component to be able to accept a ref.

The downside is that you need to write much more code when using the SingleChildOnly component.

I thought of creating TextWithTooltip ButtonWithTooltip base components wrapping only a simple html span or button.

Don't know if it's applicable for your use case

Upvotes: 1

jered
jered

Reputation: 11571

No.

There is no way to enforce that an instance of a React component is utilizing React.forwardRef(). That's because React doesn't preserve the type of the original function or class a component instance was created with. An instance of a React component in JSX will always be JSX.Element, React.FunctionComponentElement<...>, or React.CElement<...>.

Example:

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

However, you can enforce that a React component only accepts a single child element, and TypeScript will warn you accordingly. This is about as good as you can hope for you in your case.

Example:

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

Upvotes: 1

Related Questions