Reputation: 85
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:
<button>
, <div>
)forwardRef
wrapped function componentIt 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
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
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