Skip to content

How to easily work with Portable text in React

Having built projects with text editors that implemented their own custom way of storing text while keeping the same HTML structure and tags, my discovery of Portable text spec felt like a heaven-sent. However this discovery soon turned into a node package hell trying to figure out how to then parse portable text into custom react components.


TLDR: This article uses react-portable-text package and I highly recommend going through their quick start example if you’re in a hurry.

This article shares my convention of creating custom typed components that deal with Portable text (in my case from my Sanity.io Portfolio.

Goal: Create a custom component that parses portable text and custom portable text types into premade components or tailwind styled html elements.

Install React-Portable-Text (docs: https://www.npmjs.com/package/react-portable-text)

yarn add react-portable-text

Create a Portable Text Parser component.

1import PortableText from "react-portable-text"
2
3interface Props {
4content: : object[] 
5/* I created a type for content to give me better type definitions 
6but a generic type will suffice  PortfolioPieceType['body']*/;
7className?: string; /* optional but for static pages I want to have 
8diffrent section spacing etc. */
9};
10
11const PortableTextParser = ({ content, className }: Props) => {
12if (!content) return null;
13
14PortableText
15className={className}
16content={content}
17serializers={{/* Serializers will go here` */}}
18}

You could then call the component like this:

1// ...
2<PortableTextParser
3    content={piece.body}
4    className={'grid gap-14 md:gap-22 lg:gap-24 '}
5/>
6// ...

Creating custom components

1// ...
2
3serializers={{
4	// An example of a custom H3 component
5	h3: ({children, ...rest}) => (<H3  className='mb-2' {...rest}>{children}</H3>),
6	
7	// An example of a statically styled component
8	li: ({ children }) => (
9	  <li className='ml-8 relative before:block before:absolute before:left-[-24px] before:content-[">"] before:text-gray-400 before:font-bold before:text-xs before:top-[5px]'>
10	    {children}
11	  </li>),
12
13	// An example of what not to do; the library automatically serializes generic html elements so if you are not styling anything just let the library do it for you. 
14	ul: ({ children }) => <ul>{children}</ul>,
15}}
16
17// ...

Creating serializers for custom blocks that may need to recursively call this portable text parser component.

Lets say we have a text image block like the image above where:

  • We have an image (which needs to be transformed separately.
  • A heading which is just a string.
  • Description text which needs to be parsed back to this component (since it contain headings, lists, images etc.

The React component

1import { SanityReference } from '@sanity/image-url/lib/types/types';
2
3export interface TextImageType  {
4  _type: 'textImage';
5  description: SanityBlockContentType[];
6  heading?: string;
7  image: SanityReferenceType;
8};
9
10
11const TextImageBlock = ({ content }: TextImageType ) => {
12  const { description, heading, image, link, linkText, textLast } = content;
13
14  if (!image?.asset?._ref) return null; 
15	/* While I have validation rules to make sure this component can't be used without an 
16	I am a big fan of coding defensively */
17
18  return (
19    <article className='text-image--container'>
20      <div className='text-image--image-container'>
21        <SanityImage src={image} width={1280} height={720} lazy/>
22      </div>
23      <div className='text-image--content-container'>
24        {heading && (<H2 as='h2'>{heading}</H2>)}
25        {description && (<PortableTextParser content={description} className='grid gap-4'/>)}
26      </div>
27    </article >
28  );
29};
30
31export default TextImageBlock;

The Serializer

1// ...
2serializers={{
3	textImage: (value: TextImageType) => <TextImageBlock content={value} />,
4}}
5// ...

Hope that that helps but in case something is missing here I highly recommend going through the react-portable-text documentation here: https://www.npmjs.com/package/react-portable-text.