Type 'Element[]' is missing the following properties when handling react children - reactjs

I have a parent component that provides its children to a child component. The children can either be a node or an array of nodes, and depending on if it is a node or an array of nodes the children in the ChildComponent are rendered differently. However, when I a render the ChildComponent in the ParentComponent, I get the following Typescript error:
TS2786: 'ChildComponent' cannot be used as a JSX component.   Its return type 'Element | Element[]' is not a valid JSX element.     Type 'Element[]' is missing the following properties from type 'Element': type, props, key
Please see the below for the code. How can I render the ChildComponent without any Typescript errors? Thanks!
import React from 'react';
import styles from './component.scss';
interface Props {
children: React.ReactChildren;
}
const ChildComponent = ({ children }: Props): JSX.Element | JSX.Element[] => {
if (React.Children.count(children)) {
return React.Children.map(children, (child) => (
<div className={styles.childContainer}>{child}</div>
));
}
return <div className={styles.singleContainer}>{children}</div>;
};
const ParentComponent: React.FC<Props> = ({ children }) => (
<div className={styles.container}>
<ChildComponent>{children}</ChildComponent>
</div>
);
export default ParentComponent;

In your current code, the return type is JSX.Element[] from if block (incorrect) but JSX.Element (correct) from else part. That's the reason of the error you are seeing.
To fix it, you need to return single JSX expression from ChildComponent as "JSX expressions must have one parent element".
You can use a parent element like div, span or simply use React.Fragment (shorthand <></>):
const ChildComponent = ({ children }: Props) => {
if (React.Children.count(children)) {
return (
<> // HERE
{React.Children.map(children, (child) => (
<div className={styles.childContainer}>{child}</div>
))}
</>
);
}
return <div className={styles.singleContainer}>{children}</div>;
};
And children type should be ReactNode instead of ReactChildren:
interface Props {
children: React.ReactNode;
}
As these are the definitions of both the typings:
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
interface ReactChildren {
map<T, C>(children: C | C[], fn: (child: C, index: number) => T):
C extends null | undefined ? C : Array<Exclude<T, boolean | null | undefined>>;
forEach<C>(children: C | C[], fn: (child: C, index: number) => void): void;
count(children: any): number;
only<C>(children: C): C extends any[] ? never : C;
toArray(children: ReactNode | ReactNode[]): Array<Exclude<ReactNode, boolean | null | undefined>>;
}
Also, I would suggest not to use React.FC.
Here is a demo.
Edit:
PS: The else block <div className={styles.singleContainer}>{children}</div> and the if condition with count is not really required.

Related

Component with function child yields error "Its return type 'ReactNode' is not a valid JSX element."

Consider the following react code: (codesandbox)
import type { ReactNode } from 'react';
type RenderCallbackProps = {
children: (o: { a: number }) => ReactNode,
};
function RenderCallback({ children }: RenderCallbackProps) {
return (children({ a: 123 }));
}
export default function App() {
return (
<RenderCallback>
{({ a }) => (
<h1>{a}</h1>
)}
</RenderCallback>
);
}
This causes the error message:
function RenderCallback({ children }: RenderCallbackProps): ReactNode
'RenderCallback' cannot be used as a JSX component.
Its return type 'ReactNode' is not a valid JSX element.
Type 'undefined' is not assignable to type 'Element | null'.ts(2786)
Why?
Where is the undefined coming from?
How to fix that?
Where is the undefined coming from?
That's part of what ReactNode means. ReactNode is: ReactChild | ReactFragment | ReactPortal | boolean | null | undefined, or basically all of the stuff that you could legally put as the child of a <div>. But while you can put a undefined as a child of a div, it can't be the only thing returned by a component, so you get an error when you try to do return (children({ a: 123 }));
You can fix this one of two ways. If you want to continue to allow null/undefined/boolean etc to be passed in, then just make sure that RenderCallback always wraps it in an element. A fragment will be enough:
function RenderCallback({ children }: RenderCallbackProps) {
return (
<>
{children({ a: 123 })}
</>
)
}
Or if you don't want to allow those values, then you can change the types on the callback. For example only allowing elements:
children: (o: { a: number }) => ReactElement,

Index children of React component with TypeScript

I'm making a component that has multiple sets of children.
The question React component with two sets of children suggests to index children, as in props.children[0]. This works great in JavaScript.
But in TypeScript, I'm getting a type error, even though the code works fine at runtime.
function MyComponent(props: { children: React.ReactNode }) {
return <>
...
{props.children[0]}
...
{props.children[1]}
...
</>;
}
TypeScript fails with the following error messages on props.children[0]:
Object is possibly 'null' or 'undefined'.
ts(2533)
Element implicitly has an 'any' type because expression of type '0' can't be used to index type 'string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal'.
Property '0' does not exist on type 'string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal'.
ts(7053)
How do I make it typecheck?
You need to tell Typescript that your component needs multiple children by putting the array brackets:
function MyComponent(props: { children: React.ReactNode[] }) {
// ...
}
If your component accepts exactly 2 children, a tuple type would be preferable so that Typescript will enforce having 2 children:
function MyComponent(props: { children: [React.ReactNode, React.ReactNode] }) {
// ...
}
If your component accepts 2 or more children, you can specify a tuple with a rest element:
function MyComponent(props: { children: [React.ReactNode, ...React.ReactNode[]] }) {
// ...
}
You can use ReactNode[] instead of ReactNode
function MyComponent(props: { children: React.ReactNode[] }) {
return <>
{props.children[0]}
{props.children[1]}
</>;
}
function TestComponent() {
return <MyComponent>
<span>Element 1</span>
<span>Element 2</span>
{'string test'}
{2}
{true}
</MyComponent>
}
Playground
If your component always attempts to render at least 2 child nodes, then you should explicitly type the children parameter that way:
TS Playground
import {type ReactElement, type ReactNode} from 'react';
function MyComponent(props: {
children: [ReactNode, ReactNode, ...readonly ReactNode[]];
}): ReactElement {
return <>
...
{props.children[0]}
...
{props.children[1]}
...
</>;
}
By doing so, TypeScript will emit a compiler error diagnostic if you don't provide at least two children:
function App (): ReactElement {
return (
<MyComponent>{/*
~~~~~~~~~~~
Type '{ children: []; }' is not assignable to type '{ children: [ReactNode, ReactNode, ...ReactNode[]]; }'.
Types of property 'children' are incompatible.
Type '[]' is not assignable to type '[ReactNode, ReactNode, ...ReactNode[]]'.
Source has 0 element(s) but target requires 2.(2322) */}
</MyComponent>
);
}
Similarly, when you only provide one child node:
TS Playground
function App (): ReactElement {
return (
<MyComponent>{/*
~~~~~~~~~~~
This JSX tag's 'children' prop expects type '[ReactNode, ReactNode, ...ReactNode[]]'
which requires multiple children, but only a single child was provided.(2745) */}
<div></div>
</MyComponent>
);
}
But as soon as you provide at least two, the diagnostic disappears:
TS Playground
function App (): ReactElement {
return (
<MyComponent>
<div></div>
<div></div>
</MyComponent>
);
}

Detecting React children array length in Typescript

I have a component that can accept React children as either a node or an array of nodes. I want to be able to detect if children is an array of nodes, but I am getting the following Typescript error:
TS2339: Property 'length' does not exist on type 'string | number | true | {} | ReactElement<any, string | ((props: any) => ReactElement<any, any> | null) | (new (props: any) => Component<any, any, any>)> | ... 47 more ... | (ReactNode[] & ReactPortal)'.   Property 'length' does not exist on type 'number'.
Is there a way in Typescript where I detect if children has length? Thanks.
import React from 'react';
interface Props {
children: React.ReactNode | React.ReactNode[];
}
const SampleComponent: React.FC<Props> = ({ children }) => {
if (children && children.length) {
return children.map((child) => (
<div>{child}</div>
));
}
return children;
};
export default SampleComponent;
There is a React.Children API for dealing with the props.children (which is an opaque data structure).
Also, React.Children has methods like count, map etc.
if (React.Children.count(children)) {
return React.Children.map(children, (child) => <div>{ ..todo.. }</div>)
}
It has TypeScript support too. So, you won't need to cast the types.
[Update]
Please check out Ajeet Shah's response with using React.Children API.
[Original]
Cast children to React.ReactNode[] before checking length like this:
(children as React.ReactNode[]).length
You could also create a type predicate:
https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
Try React.Children.count(children)
The best way in this case is not use Reacf.FC
Defined the props like const MyButton = (props: MyProps)=>...
Or use React.VFC
Function Components - Why is React.FC discouraged?

What is proper type for React's children as function?

I'm passing a children as a function, returning React.ReactNode into Provider HOC like follows:
<Provider variables={{ id: "qwerty" }}>
{data => (
<ExampleComponent data={data} />
)}
</Provider>
Here's a provider component example code:
type Props = {
id: string;
children: any; // What type should be put here?
};
const Provider: React.SFC<Props> = ({ id, children }) => {
const { data, loading, error } = useQuery<req, reqVariables>(query, {
variables: { id }
});
if (loading) return "Loading...";
if (error) return "Error";
if (!data) return "No data";
return children(data);
}
If I'm not specifying any type for children, so leaving it to default to React.ReactNode, then line return children(data); shows an error:
Cannot invoke an expression whose type lacks a call signature. Type 'string | number | boolean | {} | ReactElement ReactElement Component)> | null) | (new (props: any) => Component)> | ReactNodeArray | ReactPortal' has no compatible call signatures.
If I'm specifying children explicitly in type Props as any, everything works great, but it's Wrong from a Typescript and types perspective.
What is a proper type to be put for props.children, when a function returning React.ReactNode is passed instead of React.ReactNode itself, so Typescript would accept it as a valid "Render function" and will allow calling children like function?
You can specify the children type to be a function which returns a ReactNode
type Props = {
id: string,
children: (props: InjectedCounterProps) => React.ReactNode
};
JSX.Element can also be the type, in case the children being 'DOM element'.

Typing a dynamic tag in React with TypeScript?

How do I type a dynamic tag in React with TypeScript? Given this code:
interface CompProps {
tag: string;
}
const MyComponent: React.FunctionComponent<CompProps> = ({
tag = "div",
children
}) => {
const Wrapper = tag;
return <Wrapper>{children}</Wrapper>;
};
I am getting this error:
Type '{ children: ReactNode; }' has no properties in common with type 'IntrinsicAttributes'. ts(2559)
It seems to me I have to add proper types but I cannot figure out which.
You can pass in a string as a tag name and use that as you have, but you need to type it properly to get type checking to work. tag should be a key of JSX.IntrinsicElements.
interface CompProps {
tag: keyof JSX.IntrinsicElements;
}
const MyComponent: React.FunctionComponent<CompProps & React.HTMLAttributes<HTMLOrSVGElement>> = ({
tag: Wrapper = "div",
children,
...rest
}) => {
return <Wrapper {...rest}>{children}</Wrapper>;
};
Playground Link
Using A Type definition For All HTML Elements
In order to allow all HTML elements to be used as your tag, you can utilize the keys of the IntrinsicElements interface defined in the JSX namespace. IntrinsicElements appears to contain a mapping of HTML element tags to their respective attributes (includes element-specific attributes). To utilize these keys we can do the following:
interface Props {
tag?: keyof JSX.IntrinsicElements
}
What if I want to allow React components to be used as the tag?
React defines two interfaces: ComponentClass and FunctionComponent. React also defines a union of these two interfaces that allows you to specify any React component: ComponentType. We can create a union of this and our last definition to allow both components and HTML tags.
import { ComponentType } from 'react';
interface Props {
tag?: ComponentType | keyof JSX.IntrinsicElements;
}
Well, now I have a tag, what about HTML attributes?
If you want to allow all other HTML attributes to be allowed you can either extend React.HTMLAttributes<Element> to get all of the shared HTML attributes (no element-specific ones) or you can introduce a generic and utilize JSX.IntrinsicElements.
The second option is more complex and comes with a few caveats. You have to use type instead of interface to extend/intersect your Props and the specific attributes defined on a key in JSX.IntrinsicElements. You will also need to use generics on your function so that you can pass them to your Props type which means you can no longer use React.FunctionComponent<Props> since that happens before access to any generics. This means you'll want to add children to your Props definition.
That was a lot of words which I believe are better explained with this example:
// Define our Props type to allow the specifying of a Tag for HTML attributes
// Also define children as React does with React.ReactNode
type Props<Tag extends keyof JSX.IntrinsicElements> = {
tag?: ComponentType | keyof JSX.IntrinsicElements;
children?: ReactNode;
} & JSX.IntrinsicElements[Tag];
// Define our generic (Tag) again here and give it our default value
// Don't forget to specify the type Props<Tag> at the end of your function's arguments
// Then we can spread all props to the tag/Wrapper
function MyComponent<Tag extends keyof JSX.IntrinsicElements = 'div'>({ tag: Wrapper = 'div', ...props }: Props<Tag>) {
return <Wrapper {...props} />;
}
// Example usage, noValidate is typed as
// (JSX attribute) React.FormHTMLAttributes<HTMLFormElement>.noValidate?: boolean | undefined
<MyComponent<'form'> tag="form" noValidate>
{/* My Form Stuff */}
</MyComponent>;
// You don't need to specify 'div' since it is the default
<MyComponent id="page">
<p>Just a paragraph inside of a regular div</p>
</MyComponent>;
I had a similar problem where I tried to generate a dynamic heading tag based on a passed 'level' prop. It also generated the "Property X does not exist on type IntrinsicAttributes" error.
The code that generated the error was the following;
// Heading.tsx
import React, { FunctionComponent, ReactNode } from 'react';
interface PropsType {
level: 1 | 2 | 3 | 5 | 6;
children?: ReactNode;
}
type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
const HeadingComponent: FunctionComponent = ({
level,
children = null
}: PropsType) => {
const Tag = `h${level}` as HeadingTag;
return (
<Tag>
{children}
</Tag>
);
};
export default HeadingComponent;
// And I used this component all over my codebase like this;
// HomePage.tsx
<Heading level={1}>
This Is A Title
</Heading>
I solved this by changing:
const HeadingComponent: FunctionComponent = ({
... // removed for brevity
}: PropsType) => {
... // removed for brevity
};
to:
const HeadingComponent: FunctionComponent<PropsType> = ({
... // removed for brevity
}) => {
... // removed for brevity
};
const YourComponent: React.FC<Props> = ({ tag: Tag = 'button', children, ...props }) => (
<Tag {...props}>
{children}
</Tag>
);
type Props = {
tag?: keyof JSX.IntrinsicElements;
} & React.HTMLAttributes<HTMLOrSVGElement>;
This works well for me.
Simple method to use dynamic tag name in React with TypeScript:
export default function Text(props: TextProps) {
const { text, header } = props;
let Tag: string;
if (!header) Tag = "span";
else Tag = `h${header}`;
const ConstTag = Tag as "span" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
return <ConstTag>{text}</ConstTag>;
}

Resources