Updating React 17 to React 18 - Typescript - Children of ReactNode type no longer handles inline conditional rendering of `void`

Viewed 2088

When using the React.ReactNode type for children if there are any inline conditional renders it will fail. I am currently using SWR to fetch data so I get an error like this:

Type 'false | void | Element | undefined' is not assignable to type 'ReactNode'.
 Type 'void' is not assignable to type 'ReactNode'

I'm not sure if previously React.ReactNode allowed void or if it was just silently missed due to implicit children before. Maybe this is an issue I should open up with React or SWR?

Previously

My React 17 Page component looked something like this previously. There is a lot of information around the breaking changes of implicit children but I always explicitly stated mine. Maybe a child of type: void, undefined, or null would be considered an implicit child though?

type PageProps = {
  children: React.ReactNode
}

const Page = ({ children }: PageProps) = > {
  return (
    <div>
      <Header />

      <div>
        {children}
      </div>

      <Footer />
    </div>
  )
}

export default Page

Problems

First Issue "multiple children were provided"

This JSX tag's 'children' prop expects a single child of type
 'ReactNode', but multiple children were provided.ts(2746)

Fix

That's fixed simply enough with this type that allows multiple or single children. I do wish we could get away without being explicit about or many, but it does give stronger typing.

type PageProps = {
  children: React.ReactNode[] | React.ReactNode;
};

Example Page Failing using SWR

const UsersPage: NextPage = () => {
  const { data, error } = useSWR('/api/user', fetcher)

  return (
    <Page>
      {users && (
        <pre>
          {JSON.stringify(users, null, 2)}
        </pre>
      )}
    </Page>
  )
}

This will give me the following error:

Type 'false | void | Element | undefined' is not assignable to type 'ReactNode'.
 Type 'void' is not assignable to type 'ReactNode'
1 Answers

Option 1: Add void to the ReactNode type

One way would be to allow void on the type like so:

type ReactNode = React.ReactNode | void;

type PageProps = {
  children: ReactNode[] | ReactNode;
};

An Error Occurs

You will end up this confusing error given the above implementation of the Page component:

This JSX tag's 'children' prop expects a single child of type 'ReactNode',
 but multiple children were provided.ts(2746)

This is confusing because it's complaining about single vs multiple children, yet if you remove void from the new typing it will be fine, so this seems to not be about whether it's an array or not.

This can be resolved by wrapping the {children} render like so: <>{children}</>

Example

type ReactNode = React.ReactNode | void;

type PageProps = {
  children: ReactNode[] | ReactNode;
};

const Page = ({ children }: PageProps) = > {
  return (
    <div>
      <Header />

      <div>
        <>{children}</>
      </div>

      <Footer />
    </div>
  )
}

export default Page

Option 2: Handle the void

Another option would be to handle the void by making sure that if the conditional inline render returns void default to an empty React fragment (<></>) like so:

const UsersPage: NextPage = () => {
  const { data, error } = useSWR('/api/user', fetcher)

  return (
    <Page>
      {(users && (
        <pre>
          {JSON.stringify(users, null, 2)}
        </pre>
      )) || <></>}
    </Page>
  )
}

Closing Notes

Although Option 2 may be considered more "proper" rather than overriding and handling the ReactNode type, this would start to get redundant and feels like an anti-pattern at that point. Option 2 would also require you to update anywhere with a conditional render which gets cumbersome. Option 1 patches it so that you can continue utilizing void returns. I'm not certain if there is a downside to this

⭐️ A Further Dig ⭐️ -- The Real Answer

useSwr returns the types of the fetcher, if the fetcher returns void then you will run into this problem, if the fetcher does not return void then this problem can be avoided completely as useSWR will return the callback value or undefined which ReactNode can handle [undefined, false, null].

You will not even need to change the children property on the Page component if useSwr does not return void. You can simply keep it as: children: React.ReactNode since ReactNode includes ReactFragment which is an iterable ReactNode which includes single or multiple children.

ReactNode Type from React Type Definition

type ReactNode = ReactElement | string | number | ReactFragment | ReactPortal | boolean | null | undefined;
Related