Troubleshooting Portable Text implementation in Sanity/Next.js project
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!
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.