Troubleshooting Portable Text implementation in Sanity/Next.js project

5 replies
Last updated: Aug 23, 2024
Hey Team! I am trying to implement Portable Text into my Sanity/Next project. I am using the template template-nextjs-personal-website which uses
_import_ {
PortableText,

_type_ PortableTextBlock,

_type_ PortableTextComponents,
}
_from_ 'next-sanity'

I am stuck with this error and have been triple checking everything, has anyone ran into this?

Error: Objects are not valid as a React child (found: object with keys {markDefs, children, _type, style, _key}). If you meant to render a collection of children, use an array instead.



        defineField({
          name: 'optionalText',
          title: 'Optional Text',
          type: 'array',
          of: [
            defineArrayMember({
              lists: [],
              marks: {
                annotations: [
                  {
                    name: 'link',
                    type: 'object',
                    title: 'Link',
                    fields: [
                      {
                        name: 'href',
                        type: 'url',
                        title: 'Url',
                      },
                    ],
                  },
                ],
                decorators: [
                  {
                    title: 'Italic',
                    value: 'em',
                  },
                  {
                    title: 'Strong',
                    value: 'strong',
                  },
                ],
              },
              styles: [],
              type: 'block',
            }),
          ],
          validation: (rule) => rule.max(155).required(),
        }),
Trying to render

<CustomPortableText
   paragraphClasses="font-serif max-w-3xl text-gray-600 text-xl"
   value={hero.optionalText}
/>
AI Update

I've seen this exact error before! The issue is that your optionalText field is being returned from Sanity as an array of Portable Text blocks, but somewhere in your code you're likely trying to render the individual block objects directly instead of passing the array to the PortableText component.

The error message Objects are not valid as a React child (found: object with keys {markDefs, children, _type, style, _key}) is telling you that React is receiving a raw Portable Text block object (with those internal keys) instead of properly rendered content.

Here are the most common causes and fixes:

1. Check your GROQ query

Make sure you're querying optionalText correctly. It should return the full array:

// âś… Correct
const query = `*[_type == "yourType"][0]{
  optionalText
}`

// ❌ Incorrect - don't access array elements or nested properties
const query = `*[_type == "yourType"][0]{
  optionalText[0]
}`

2. Verify the data structure

Before passing to CustomPortableText, log the value to confirm it's an array:

console.log('optionalText:', hero.optionalText)
// Should be: [{_type: 'block', ...}, {_type: 'block', ...}]
// Not: {_type: 'block', markDefs: [], ...}

3. Check for conditional rendering issues

If you're conditionally rendering or mapping over data, make sure you're not accidentally passing a single block object:

// ❌ Wrong - mapping creates individual blocks
{hero.optionalText?.map(block => (
  <CustomPortableText value={block} />
))}

// âś… Correct - pass the whole array
<CustomPortableText value={hero.optionalText} />

4. Ensure CustomPortableText is set up correctly

Your CustomPortableText component should be using the PortableText component from next-sanity properly. Based on the @portabletext/react documentation, it should look something like:

import { PortableText, type PortableTextComponents } from 'next-sanity'

export function CustomPortableText({ 
  value, 
  paragraphClasses 
}: { 
  value: any
  paragraphClasses?: string 
}) {
  const components: PortableTextComponents = {
    block: {
      normal: ({ children }) => (
        <p className={paragraphClasses}>{children}</p>
      ),
    },
  }

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

5. Check for Next.js 15 async params

If you're using Next.js 15, make sure you're awaiting params if you're in a page component:

// Next.js 15
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  // ... fetch data
}

The most likely culprit is either your GROQ query returning a single block instead of an array, or somewhere in your component tree you're accidentally passing hero.optionalText[0] or accessing a nested property that returns a single block object.

If you're still stuck, share your GROQ query and the output of console.log(typeof hero.optionalText, hero.optionalText) right before the <CustomPortableText> component, and we can narrow it down further!

👋 My initial thought is you’re getting that error because you’re not passing a component in to handle your
link
annotation.
Hi
user M
thank you for the quick response! I have a component that I am importing in to handle the link annotations

import {
  PortableText,
  type PortableTextBlock,
  type PortableTextComponents,
} from 'next-sanity'
import type { Image } from 'sanity'

import ImageBox from '@/components/shared/ImageBox'
import { TimelineSection } from '@/components/shared/TimelineSection'

export function CustomPortableText({
  paragraphClasses = 'my-4', // Default paragraph classes
  value,
}: {
  paragraphClasses?: string
  value: PortableTextBlock[] | undefined // Allow for undefined values
}) {
  console.log('CustomPortableText value:', JSON.stringify(value, null, 2))
  if (!Array.isArray(value)) {
    console.error(
      'Invalid value prop passed to CustomPortableText component. Expected an array of PortableTextBlock objects.',
    )
    return null
  }
  const components: PortableTextComponents = {
    block: {
      normal: ({ children }) => {
        if (Array.isArray(children)) {
          return children.map((child, index) => (
            <p key={index} className={paragraphClasses}>
              {child}
            </p>
          ));
        } else {
          return <p className={paragraphClasses}>{children}</p>;
        }
      },
    },
    marks: {
      link: ({ children, value }) => {
        return (
          <a
            className="underline transition hover:opacity-50"
            href={value?.href}
            rel="noreferrer noopener"
          >
            {children}
          </a>
        )
      },
    },
    types: {
      image: ({
        value,
      }: {
        value: Image & { alt?: string; caption?: string }
      }) => {
        if (!value) return null // Return null if value is not defined

        return (
          <div className="my-6 space-y-2">
            <ImageBox
              image={value}
              alt={value.alt || 'Image'} // Default alt text if none is provided
              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 || []} /> // Ensure items is defined
      },
    },
  }
  console.log('CustomPortableText value:', JSON.stringify(value, null, 2))
  // Render only if value is defined and is an array
  if (!value || !Array.isArray(value)) return null

  return <PortableText components={components} value={value} />
}
Got it! It doesn’t look like the link is the issue. I can’t tell where it would be coming from, since it’s possible it’s happening anywhere that you’re passing down
child
or
children
, or even in one of the other components you’ve defined. Can you try commenting them out and adding them back one by one to narrow down which is responsible?
I have been trying to get this to work for a couple days, I just reloaded everything and made sure the data in Sanity was still present. The app is now loading and rendering the data, not sure what the error was which is the most frustrating part. BUT I think it could have been the data being null in Sanity after editing the schema..
Anyway, its fixed now thank you for your time
user M
Glad it’s working!

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?