Migrating from react-portable-text to @portabletext/react value prop
const query = `*[_type == "post" && slug.current == $slug][0] {
_id,
_createdAt,
title,
description,
mainImage,
slug,
body[]{
...,
asset -> {
...,
"_key": _id
}
},
'comments': *[
_type == "comment" &&
post._ref == ^._id &&
approved == true
],
author -> {
name,
image
}
}`;<PortableText
value={post.body}
components={{
types: {
image: ({ asset, alt, width }) => (
<figure className="inline-image">
<img
src={urlFor(asset).width(width).fit("max").url()}
alt={alt}
/>
<figcaption>
{caption && <span className="float-left">{caption}</span>}
{credit && (
<i className="float-right">Photo credit: {credit}</i>
)}
</figcaption>
</figure>
),
},
block: {
h1: (props: any) => (
<h1 className="text-2xl font-bold my-5" {...props} />
),
h2: (props: any) => (
<h2 className="text-xl font-bold my-5" {...props} />
),
h3: (props: any) => (
<h3 className="text-lg font-bold my-5" {...props} />
),
h4: (props: any) => (
<h4 className="text-md font-bold my-5" {...props} />
),
blockquote: (props: any) => (
<blockquote
className="text-xl text-red-300 font-bold my-5"
{...props}
/>
),
},
marks: {
link: ({ href, children }: any) => (
<a href={href} className="text-blue-500 hover:underline">
{children}
</a>
),
},
listItem: {
li: ({ children }: any) => (
<li className="ml-4 list-disc">{children}</li>
),
},
}}
/>Your code looks correct! The value={post.body} prop is indeed the right approach when migrating from react-portable-text (which used content) to the newer @portabletext/react (which uses value).
Based on your GROQ query and component setup, the issue is likely in how you're handling the image type in your custom components. The @portabletext/react library passes props differently than the old library.
Here's what needs to change in your image component:
<PortableText
value={post.body}
components={{
types: {
image: ({ value }) => { // Changed from destructuring asset directly
return (
<figure className="inline-image">
<img
src={urlFor(value.asset).width(value.width || 800).fit("max").url()}
alt={value.alt || ''}
/>
<figcaption>
{value.caption && <span className="float-left">{value.caption}</span>}
{value.credit && (
<i className="float-right">Photo credit: {value.credit}</i>
)}
</figcaption>
</figure>
);
},
},
// ... rest of your components
}}
/>The key difference: in @portabletext/react, custom type components receive a value prop that contains all the block's data, whereas the old library passed properties directly. So you need to access value.asset, value.alt, value.caption, etc.
Also note that you were referencing caption and credit variables that weren't defined - you'll need to access them from value.caption and value.credit.
Your GROQ query looks good for fetching the body content. The value={post.body} is correct - it should be the array of blocks returned from your query.
If you're still getting errors, check:
- That
post.bodyis actually defined and is an array - The exact error message - it will help identify if there's a specific block type or mark causing issues
- That you've installed the correct package:
npm install @portabletext/react(notreact-portable-text)
The Portable Text introduction guide and beginner's guide have more examples of custom serializers if you need additional reference.
Show original thread28 replies
Was this answer helpful?
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.