Discussion about fixing a type error in PortableText with React 17/18

33 replies
Last updated: Aug 25, 2022
How do I fix this in PortableText?
Type error: 'Li' cannot be used as a JSX component.
   Its element type 'ReactElement<any, any> | Component<PortableTextComponentProps<PortableTextListItemBlock<PortableTextMarkDefinition, PortableTextSpan, string, string>>, any, any>' is not a valid JSX element.
AI Update

This TypeScript error occurs when defining custom list item components in @portabletext/react due to type incompatibility between what TypeScript expects for a JSX component and what you're providing.

The issue typically happens when you define a listItem component in your components object. The error message indicates that TypeScript sees the Li component type as ReactElement | Component, which isn't a valid JSX element type signature.

The Solution:

Make sure your listItem component is defined as a proper functional component that returns JSX, not as a class component or mixed type. Here's the correct way to type it:

import { PortableText, PortableTextComponents } from '@portabletext/react'

const components: PortableTextComponents = {
  list: {
    bullet: ({children}) => <ul className="custom-bullet-list">{children}</ul>,
    number: ({children}) => <ol className="custom-number-list">{children}</ol>,
  },
  listItem: {
    bullet: ({children}) => <li className="custom-bullet-item">{children}</li>,
    number: ({children}) => <li className="custom-number-item">{children}</li>,
  },
}

// Then use it:
<PortableText value={portableTextContent} components={components} />

Key Points:

  1. Use arrow functions: Define your components as arrow functions that accept props and return JSX directly
  2. Separate list and listItem: The list property controls the wrapper (<ul> or <ol>), while listItem controls individual items (<li>)
  3. Match list types: Both list and listItem can have bullet and number properties to handle different list styles

If you're using custom types:

import type { PortableTextComponents, PortableTextListItemBlock } from '@portabletext/react'

const components: PortableTextComponents = {
  listItem: ({children, value}) => {
    // value gives you access to the full block data if needed
    return <li>{children}</li>
  }
}

The error usually resolves when you ensure your component definitions are consistently typed as functional components. Avoid mixing class components, or returning conditional types that might be ReactElement in some cases and Component in others. Keep them as simple functional components that always return JSX.

For more details on customizing Portable Text rendering, check out the guide on customizing Portable Text from schema to React component.

I tried upgrading React to 18.2.0 but then next wouldn't be found in Vercel
Is this within the Studio?
No, a client
Can you share more information? What does your code look like? What context does it happen in?
Its a portabletext render. Of blocks from sanity.
Alright. We're going to be unable to help you if you don't share significantly more information about your code and what you're trying to do.
const components: Partial<PortableTextReactComponents> = {
  block: {
    normal: ({ children }) => <p className="text-lg">{children}</p>,
    h1: ({ children }) => <h1 className="text-3xl">{children}</h1>,
    h2: ({ children }) => <h2 className="text-2xl">{children}</h2>,
  },
  types: {
    wideartwork: ({ value }: { value: Artwork }) => {
      return (
        <Link href={`/artworks/all/${value._id}`}>
          <a>
            <NextSanityImage
              image={value.image}
              width={1900}
              height={1200}
              className="my-12 lg:my-20 max-h-[80vh] object-contain"
            />
          </a>
        </Link>
      );
    },
    splitartworks: ({
      value,
    }: {
      value: { artworkleft: Artwork; artworkright: Artwork };
    }) => {
      return (
        <div className="grid grid-cols-2 my-12 lg:my-20 gap-4 2xl:gap-12">
          <Link href={`/artworks/all/${value.artworkleft._id}`}>
            <a>
              <NextSanityImage
                image={value.artworkleft.image}
                width={950}
                height={500}
              />
            </a>
          </Link>

          <Link href={`/artworks/all/${value.artworkright._id}`}>
            <a>
              <NextSanityImage
                image={value.artworkright.image}
                width={950}
                height={500}
              />
            </a>
          </Link>
        </div>
      );
    },
  },
};

<PortableText value={project.content} components={components} />
Sorry. Ofc. Is that better?
project.content is is a an array of blocks with different types. No where do I have a list of an Li.
No wait I do. It's a different data set. Maybe adding the
normal
block is what breaks it.
Happens locally too now
Got it. Thanks for sharing your code.
I have a couple of thoughts. First, I wonder if it's incompatibility between different versions of React in react-portable-text and your frontend? If I recall correctly, the portable text package is on 17. Since you mentioned adding the
normal
serializer might be breaking it, does it work if you remove it? That might help us narrow it down.
I realized it had nothing to do with the content
Anything react 17 fails to build, and 18 works
No now both data sets build locally on 17 and 18. I am confused. Waiting on a build and I will see how that differs.
Maybe it's not coming from the components then. What does your usage of the PortableText component look like?
I posted it above
It's just normal text, a bullet list or a referenced image
I meant your usage of that entire PortableText component, not just the components you're passing into it for serialization.
I posted that above
<PortableText value={project.content} components={components} />

No, how are you using that component?
That is the component. That is how I am using it. Not sure what you mean?
I use it to render blocks. Text and images.
I seem to be good now. I got it working, for some reason I don't know.
Do you know how you got around this
user H
? Here's a thread about it on the packages github page but the only potential fix is React 18, something I'm reluctant to do..
No I staying on 17. Saw that thread too.
But I do want to upgrade and this is blocking.
Super weird... I've a friend working on it so will update if there's a fix found
Updating
@types/react
to v18+ was the solution for me. Discovered the answer when I ran into the same issue with an SVG component...
I already had that
Worked for me, good luck!

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?