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

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'.

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

Type 'unknown' is not assignable to other type, when strict mode is true

I have type error.
Exists child component Pagination with type of props:
interface BaseProps {
url: string;
rowKey: string;
renderItem: (item: unknown) => React.ReactNode;
params?: Record<string, unknown>;
foundTextSuffix?: string;
separated?: boolean;
useLaggy?: boolean;
}
In parent component function renderItem is passed to child component Pagination. This function needed to render other nested components in different places.
<Pagination
url={API.service('search')}
rowKey="id"
foundTextSuffix=" services"
renderItem={({id, shortTitle, organizationTitle}: ServiceExtended) => (
<ListItem
title={<Link href={{pathname: '/services/[serviceId]', query: {serviceId: id}}}>{shortTitle}</Link>}
subTitle={organizationTitle}
/>
)}
params={frozen}
/>
Argument of function renderItem has type unknown in type of Pagination component, strict mode is true. When I try to pass a function in props of Pagination component with argument type ServiceExtended, for example, there is type error:
Type 'unknown' is not assignable to type 'ServiceExtended'
I can't list all possible types in argument of renderItem, because there are a lot of them, there will be new ones in the future and this approach doesn't work with strict mode.
Please, help to find solution
I wrote simple example of my case below. This type error is occurring only with strict mode
type childPropType ={
render: (data: unknown) => JSX.Element
}
type parentPropType = {
id: number,
greeting: string
}
const data: parentPropType = {
id: 1,
greeting: 'Hello'
}
const Child = (props: childPropType) => {
const {render} = props;
return (
<div>{render(data)}</div>
)
}
const Parent = () => {
return (
<div>
<Child
render={
({id, greeting}: parentPropType) => <div>{greeting}</div>
}
/>
</div>
)
}

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

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.

Why React.FC doesn't allow me to just return children?

While trying to create a component I realized a situation.
Only returning children
interface FooProps {
children?: React.ReactNode
}
const Foo: React.FC<FooProps> = ({ children }) => {
return children
}
Will give me an error saying:
Type '({ children }: PropsWithChildren<FooProps>) => ReactNode' is not assignable to type 'FC<FooProps>'.
Type 'ReactNode' is not assignable to type 'ReactElement<any, any> | null'.
Type 'undefined' is not assignable to type 'ReactElement<any, any> | null'.
But if I return children inside any jsx, or even Fragment:
const Foo: React.FC<FooProps> = ({ children }) => {
return <>{children}</>
}
It won't give me any error.
The obvious answer to this is that the types are incompatible, ReactNode is not assignable to type ReactElement<any, any> | null, as the error says, but my question is Why?
Why returning a ReactNode (e.g. children) isn't allowed? Shouldn't it be allowed?
Something extra to ask would be if this is something about React.FC and probably other type will be ok if I return ReactNode or if this is with all react components?
Because React.ReactNode type is a union type. Let's see what the type definition for it looks like,
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
Expected return type when you create a functional component with generic FC
ReactElement<any, any> | null
So when you return the children directly from the component the compiler complains because the types are simply not compatible. The reason being ReactElement is a valid React.ReactNode but not the other way around, because React.ReactNode can also be a value of type ReactFragment or ReactPortal and so on.
It is obvious that it will create a type mismatch with ReactElement. But when you return the children inside a Fragment the compiler no longer complains because the return type becomes valid. Take a look at this example without the FC generic,
// The return type of Foo is inferred as React.ReactNode
// Compiler doesn't complain because we don't annotate the return type
const Foo = ({ children }: PropsWithChildren<FooProps>) => {
return children;
};
// But when you use React.FC generic it is the same as annotating the return type
// of Foo as `React.ReactElement`
// compiler will complain because of the type mismatch
const Foo = ({ children }: PropsWithChildren<FooProps>): React.ReactElement => {
return children;
};
Example for why it works when you return the children inside a Fragment
// Without generic FC
// Return type infered as JSX.Element which simply extends React.ReactElement
const Foo = ({ children }: PropsWithChildren<FooProps>) => {
return <>children</>;
};
// With generic FC
// Compiler doesn't complain because of JSX.Element can be valid-
// return type because it simply extends the interface React.ReactElement
const Foo: FC<FooProps> = ({ children }) => {
return <>children</>;
};
Edit - This may not be a direct answer, but I hope this example can explain more on why React.ReactNode should not be allowed.
React.ReactNode is broad, and you can assign almost anything to it. For example
const Foo = () => { return 45; };
// no compile time error because even a functions are a valid ReactNode
const Bar: React.ReactNode = Foo
But what happens when we try to render it inside any valid component
At compile time - No error
const FooBar: React.FC<FooProps> = () => {
// again no compile time error because Bar is a valid ReactNode
return <div>{Bar}</div>
}
At runtime - Error Functions are not valid react child

Resources