Migrating from react-portable-text to @portabletext/react value prop

28 replies
Last updated: Feb 20, 2022
Currently using the older PortableText plugin react-portable-text but I would like to migrate to _*@portabletext/react.* Doing so, it’s asking to add a value instead of content. using the older plugin I specify in Nextjs the content from the post.bo _dy from my Sanity GROQ query. But moving this to value is not working. What should go inside the value now? It’s not so clear from the documentation really.


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
        }
      }`;
My PortableText the value is wrong.. I am trying to load all the post->body content inside the value. But now it’s giving me errors.


<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>
                ),
              },
            }}
          />

AI Update

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:

  1. That post.body is actually defined and is an array
  2. The exact error message - it will help identify if there's a specific block type or mark causing issues
  3. That you've installed the correct package: npm install @portabletext/react (not react-portable-text)

The Portable Text introduction guide and beginner's guide have more examples of custom serializers if you need additional reference.

Show original thread
28 replies
user M
Take a look at the “value” I am passing the GROQ’s post-&gt;body to it
What error are you getting?
This
When I remove the types =&gt; image it works.
Ah, the issue is with your asset being undefined, not with value not being passed in then. I don't think the way you're querying images inside of block content correctly.
But I have an image in the content editor but it returns undefined.
The way you currently have it set you're adding a field called
asset
to each block. When a block doesn't have an asset to expand, it sets it to
null
. You're passing in a
null
value into your serializer that is now running on each block.
user A

Now it’s saying:
TypeError: Cannot read properties of undefined (reading ‘asset’)
import Header from "@components/Header";
import { sanityClient, urlFor } from "sanity";
import { Post } from "typings";
import { GetStaticProps } from "next";
// import PortableText from "react-portable-text";

import { PortableText } from "@portabletext/react";

interface Props {
  post: Post;
}

function Post({ post }: Props) {
  console.log(post);
  return (
    <main>
      <Header />

      <img
        className="w-full h-40 object-cover"
        src={urlFor(post.mainImage).url()!}
      />

      <article className="max-w-3xl mx-auto p-5">
        <h1 className="text-3xl mt-10 mb-3">{post.title}</h1>
        <h2 className="text-xl font-light text-gray-500 mb-2">
          {post.description}
        </h2>

        <div className="flex items-center space-x-2">
          <img
            className="h-10 w-10 rounded-full object-cover"
            src={urlFor(post.author.image).url()}
          />

          <p className="font-extralight text-sm">
            Blog post by{" "}
            <span className="text-green-600">{post.author.name}</span> -
            Published at{" "}
            {new Date(post._createdAt).toLocaleDateString("nl-NL", {
              year: "numeric",
              month: "2-digit",
              day: "2-digit",
            })}
          </p>
        </div>

        <div className="mt-10">
          <PortableText
            value={post.body}
            components={{
              types: {
                image: ({ node: { asset, alt, width } }: any) => (
                  <figure className="inline-image">
                    <img
                      alt={alt}
                      src={urlFor(asset).width(1280).fit("max").url()}
                    />
                  </figure>
                ),
              },
              block: {
                h1: ({ children }) => (
                  <h1 className="text-2xl font-bold my-5">{children}</h1>
                ),
                h2: ({ children }) => (
                  <h2 className="text-xl font-bold my-5">{children}</h2>
                ),
                h3: ({ children }) => (
                  <h3 className="text-lg font-bold my-5">{children}</h3>
                ),
                h4: ({ children }) => (
                  <h4 className="text-md font-bold my-5">{children}</h4>
                ),
                // 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>
                ),
              },
            }}
          />
        </div>

        <hr className="my-5 border border-yellow-500 mb-10" />
        <h3 className="text-sm text-yellow-500">Enjoyed this article?</h3>
        <h4 className="text-3xl font-bold">Leave a comment below!</h4>
        <hr className="py-3 mt-2" />

        <form className="flex flex-col py-5 mx-auto mb-10">
          <label className="block mb-5">
            <span className="text-gray-700">Name</span>
            <input
              className="shadow border rounded py-2 px-3 form-input mt-1 block w-full ring-yellow-500 outline-none focus:ring"
              placeholder="John Appleseed"
              type="text"
            />
          </label>
          <label className="block mb-5">
            <span className="text-gray-700">Email</span>
            <input
              className="shadow border rounded py-2 px-3 form-input mt-1 block w-full ring-yellow-500 outline-none focus:ring"
              placeholder="John Appleseed"
              type="text"
            />
          </label>
          <label className="block mb-5">
            <span className="text-gray-700">Comment</span>
            <textarea
              className="shadow border rounded py-2 px-3 form-textarea mt-1 block w-full ring-yellow-500 outline-none focus:ring"
              placeholder="John Appleseed"
              rows={8}
            />
          </label>
        </form>
      </article>
    </main>
  );
}

export default Post;

export const getStaticPaths = async () => {
  const query = `*[_type == "post"] {
        title,
        slug {
        current
      }
      }`;

  const posts = await sanityClient.fetch(query);

  const paths = posts.map((post: Post) => ({
    params: {
      slug: post.slug.current,
    },
  }));

  return {
    paths,
    fallback: "blocking",
  };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const query = `*[_type == "post" && slug.current == $slug][0] {
        _id,
        _createdAt,
        title,
        description,
        mainImage,
        slug,
        body[]{
          ...,
          asset -> {
          alt,
          width,
          ...,
          "_key": _id
          }
         },
        'comments': *[
            _type == "comment" &&
            post._ref == ^._id &&
            approved == true
          ],
        author -> {
         name,
         image
        }
      }`;

  const post = await sanityClient.fetch(query, {
    slug: params?.slug,
  });

  if (!post) {
    return {
      notFound: true,
    };
  }

  return {
    props: {
      post,
    },
    revalidate: 60, // After 60 seconds update old cached version.
  };
};

What does your query in getStaticProps return when you run it in Vision and pass the slug that’s causing this error?
Give me one sec
When passing only “body” to the. GROQ in the vision. I get all body items including the type image and the asset.
But on the front-end it wont show
Can you try removing the projection under
body[]
? As Racheal pointed out, asking for
asset
explicitly will look for it on every block. If you must drill down to do your
_key
thing, you’ll want to do so conditionally.

// ...
description,
mainImage,
slug,
body[],
'comments': *[
// ...
Can you try removing the projection under
body[]
? As Racheal pointed out, asking for it explicitly will cause it to look for it on everything.

// ...
description,
mainImage,
slug,
body[],
'comments': *[
// ...
Now I have this:
const query = `*[_type == "post" && slug.current == $slug][0] {
  _id,
  _createdAt,
  title,
  description,
  mainImage,
  slug,
  body[],
  'comments': *[
      _type == "comment" &&
      post._ref == ^._id &&
      approved == true
    ],
  author -> {
    name,
    image
  }
}`;

types: {
    image: ({ node: { asset, alt, width } }) => (
        <figure className="inline-image">
        <img src={urlFor(asset).width(1280).fit("max").url()} />
        </figure>
    ),
},
},

TypeError: Cannot read properties of undefined (reading 'asset')
The “node” does nothing when i console log post.body I get an array of types including the image type where the _ref is defined. But I keep getting undefined asset. Asset is not recognized while Sanity vision shows everything correctly.
The H1,H2,H3,H4, LI, OL everything works else
The H1,H2,H3,H4, LI, OL everything works else
Isn’t “node” used for GraphQL?
That suggestion was before knowing you had switched to the new portable text renderer. I would try switching it back to what you had before.
Although
node
is found in GraphQL, it's also a prop from some block content types.
user A
I have switched to the new new portable text like you have suggested. Why is the image not working? Changed the query and still no luck
user A
That was it!!! 😄 Could you please, explain me where does the “value” comes from the plugin? Because I don’t see it in the GROQ query.
Thanks
user M
and
user A
Glad to hear it, Nino.
value
comes from the renderer rather than being a property on your content, so you won’t see it in a GROQ query.
Great to see you and Geoff worked it out!!

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?