React useState error with LiteYouTubeEmbed in Sanity Portable Text

1 replies
Last updated: Jan 6, 2023
I'm trying to follow this guide to add a youtube embed to portable text and render it in a Next js frontend. I am able to get the data from portable text and display a link to the video in the front end, but when I try to use the
LiteYouTubeEmbed
component to render the Youtube embed I get
Unhandled Runtime Error

Error: React__namespace.useState is not a function
Does anyone have an updated example of how to do this?
Here's my portable text component:

import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'

import { PortableText, PortableTextComponents } from '@portabletext/react'
import { LaunchIcon,LinkIcon } from '@sanity/icons'
import ImageBox from 'components/shared/ImageBox'
import { TimelineSection } from 'components/shared/TimelineSection'
import getYouTubeId from 'get-youtube-id'
import { resolveHref } from 'lib/sanity.links'
import Link from 'next/link'
import React from 'react'
import LiteYouTubeEmbed from 'react-lite-youtube-embed';
import { Block, Image } from 'sanity'

export function CustomPortableText({
  paragraphClasses,
  value,
}: {
  paragraphClasses?: string
  value: Block[]
}) {
  const components: PortableTextComponents = {
    block: {
      normal: ({ children }) => {
        return <p className={paragraphClasses}>{children}</p>
      },
    },
    marks: {
      link: ({ children, value }) => {
        return (
          <>
            <a
              className="underline transition hover:opacity-50"
              href={value?.href}
              target={value?.blank ? '_blank' : '_self'}
              rel="noreferrer noopener"
            >
              {children}
              <LaunchIcon className='inline'/>
            </a>
            
          </>
        )
      },
      internalLink: ({children, value}) => {
        console.log(value)
        return  (
          <>
            <Link
              className="underline transition hover:opacity-50"
              href={resolveHref(value?.toType, value?.slug)}
            >
                {children}
                <LinkIcon className='inline'/>
            </Link>
            
          </>
        )
      }
    },
    types: {
      image: ({
        value,
      }: {
        value: Image & { alt?: string; caption?: string }
      }) => {
        return (
          <div className="my-6 space-y-2">
            <ImageBox
              image={value}
              alt={value.alt}
              classesWrapper="relative aspect-[16/9]"
            />
            {value?.caption && (
              <div className="font-sans text-sm text-gray-600">
                {value.caption}
              </div>
            )}
          </div>
        )
      },
      timeline: ({ value }) => {
        const { items } = value || {}
        return <TimelineSection timelines={items} />
      },
      youtube: ({value}) => {
        const { url } = value
        const id = getYouTubeId(url)
        return (
          <LiteYouTubeEmbed
            aspectHeight = {9}
            aspectWidth = {16}
            id={id}
            title={value.alt}
          />
          // <a href={value.url}>{value.alt}</a> // adding a link to youtube works fine
          // LiteYouTubeEmbed causes Unhandled Runtime Error: React__namespace.useState is not a function
        )
      }
    },
  }

  return <PortableText components={components} value={value} />
}

AI Update

The error React__namespace.useState is not a function you're encountering is a React version mismatch issue with the react-lite-youtube-embed package, not specifically a Sanity or Portable Text problem. This typically happens in Next.js when a component that uses React hooks isn't properly marked as a Client Component.

The Solution:

Since Next.js 13+ uses the App Router with Server Components by default, you need to mark your CustomPortableText component as a Client Component. Add this directive at the very top of your file:

'use client'

import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'
import { PortableText, PortableTextComponents } from '@portabletext/react'
// ... rest of your imports

The 'use client' directive tells Next.js that this component needs to run on the client side, where React hooks like useState (which LiteYouTubeEmbed uses internally) are available.

Alternative Approach - Split Components:

If you want to keep most of your Portable Text rendering on the server, you can create a separate client component just for the YouTube embed:

// components/YouTubeEmbed.tsx
'use client'

import LiteYouTubeEmbed from 'react-lite-youtube-embed'
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'

export function YouTubeEmbed({ id, title }: { id: string; title: string }) {
  return (
    <LiteYouTubeEmbed
      aspectHeight={9}
      aspectWidth={16}
      id={id}
      title={title}
    />
  )
}

Then in your main component:

// No 'use client' needed here
import { YouTubeEmbed } from './YouTubeEmbed'

export function CustomPortableText({ paragraphClasses, value }) {
  const components: PortableTextComponents = {
    // ... your other components
    types: {
      youtube: ({ value }) => {
        const id = getYouTubeId(value.url)
        return <YouTubeEmbed id={id} title={value.alt} />
      }
    }
  }
  
  return <PortableText components={components} value={value} />
}

This approach keeps your main Portable Text component as a Server Component while only the YouTube embed runs on the client.

Why This Happens:

The react-lite-youtube-embed library uses React hooks internally, which require the React runtime to be available. In Next.js Server Components, React hooks aren't available because the component is rendered on the server. The error message React__namespace.useState suggests the bundler is having trouble resolving React's exports correctly in a server context.

Your code structure looks correct for rendering Portable Text with custom components in Sanity - this is purely a Next.js Client/Server Component issue!

Show original thread
1 reply
For now I got it working using an iframe instead of using LiteYouTubeEmbed
youtube: ({value}) => {
        const { url } = value
        const id = getYouTubeId(url)
        return (
          <div className=" w-full aspect-w-16 aspect-h-9">
            <iframe
              src={`<https://www.youtube.com/embed/${id}>`}
              title={value.alt}
              allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
              allowFullScreen
            ></iframe>
          </div>
        )
      }

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?