Issue
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:
- an instance of a React class component
- a standard React HTML element (e.g.
<button>
,<div>
) - a
forwardRef
wrapped function component
It should not accept:
- a regular function component
- a text node, like a string or number
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?
Solution
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>
);
Answered By - jered
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.