@portabletext/react

repository·main·Indexed 18 days ago

https://github.com/portabletext/react-portabletext

A library for rendering Portable Text (a structured content format) within React applications. It provides a customizable component system for mapping Portable Text nodes to React components and supports both Client Components and React Server Components (RSC). Version 7 requires React 19. Includes utility types for Sanity TypeGen integration and a toPlainText() function for converting blocks to plain text strings.

Tokens
9.7K
Snippets
28
Records
34
Agent score
60%

What's inside @portabletext/react

  1. Compare `InferComponents` vs `InferStrictComponents`

    main

    Choose between these two utility types based on your required level of type safety:

    FeatureInferComponents<T>InferStrictComponents<T>
    StrictnessForgivingStrict
    HandlersAll handlers are optionalCustom types/marks/styles are required
    Extra HandlersAllowed (fallback to any)Rejected (TypeScript error)
    Best Use CaseIncremental migration or frequent schema changesProduction-grade renderers where schema/renderer sync is critical
  2. How Sanity TypeGen works with @portabletext/react

    main

    When using Sanity TypeGen, @portabletext/react automatically infers the types for custom types, marks, block styles, and list styles based on your queries. This eliminates the need for manual generics or hand-written type unions.

    The library provides three utility types to facilitate this:

    1. InferValue<T>: Derives a Portable Text array value type from a TypeGen query result type.
    2. InferComponents<T>: A forgiving component map type where all handlers are optional and extra handlers are allowed.
    3. InferStrictComponents<T>: A strict component map type that requires handlers for all inferred custom types, marks, block styles, and list styles, and rejects unknown ones.
  3. Use PortableText with React Server Components

    main

    The <PortableText> component works in both Client Components and React Server Components (RSC) without additional configuration.

    • Client Components: Loads a build optimized with the [React Compiler].
    • Server Components: Loads an uncompiled build via the react-server condition to ensure compatibility with RSC requirements.
  4. Create a re-usable `CustomPortableText` component with TypeGen

    main

    The recommended pattern for a reusable Portable Text renderer is to use InferValue for the value prop and InferStrictComponents for the components prop. This ensures that if your Sanity schema changes (e.g., a new custom type is added), TypeScript will throw an error until you provide a matching handler.

    import type { SanityQueries} from '@sanity/client'
    import {createImageUrlBuilder} from '@sanity/image-url'
    import {
      PortableText,
      type InferStrictComponents,
      type InferValue,
    } from '@portabletext/react'
    
    const builder = createImageUrlBuilder(...)
    
    // Array value type for every Portable Text item shape across all registered queries.
    type PortableTextValue = InferValue<SanityQueries[keyof SanityQueries]>
    
    export function CustomPortableText({value}: {value: PortableTextValue}) {
      const components = {
        types: {
          // `value` is fully typed from the inferred image variant.
          image: ({value}) => <img src={builder.image(value).url()} alt={value.alt || ''} />,
        },
      } satisfies InferStrictComponents<PortableTextValue>
    
      return <PortableText components={components} value={value} />
    }
  5. Strategies for choosing `InferValue<T>` input

    main

    When using InferValue<T>, always feed it query result types, not Sanity schema types. Schema types describe storage, while query results describe the actual data shape (including dereferenced fields).

    1. Preferred: Every registered query

    Use SanityQueries[keyof SanityQueries] to automatically include every Portable Text shape across all queries. This requires overloadClientMethods in sanity.cli.ts#typegen (on by default).

    type PortableTextValue = InferValue<SanityQueries[keyof SanityQueries]>

    2. Fallback: Specific named queries

    If the union from all queries is too large and slows down type-checking, narrow it to specific query results.

    import type {AuthorQueryResult, PostQueryResult} from './sanity.types'
    type PortableTextValue = InferValue<AuthorQueryResult | PostQueryResult>

    3. Alternative: A scoped mock query

    Define a focused GROQ query specifically for type generation to keep the union tight.

    import {defineQuery} from 'groq'
    const mockQuery = defineQuery(`*[_type in ['author', 'category', 'post']]{ ... }`)
    type PortableTextValue = InferValue<SanityQueries[typeof mockQuery]>
  6. Migrate to @portabletext/react v7 (React 19)

    main

    Version 7 of @portabletext/react requires React 19. This is because the client build is optimized with the React Compiler, which requires the react/compiler-runtime available in React 19.

    Key details for v7:

    • React 18 users: You must stay on @portabletext/react@6.
    • React Server Components (RSC): The package supports RSC automatically. It publishes two entrypoints via export conditions: an uncompiled build for the react-server condition (since RSC cannot load React Compiler output) and a compiled build for client components and SSR. No manual configuration is required.
    npm install @portabletext/react@7 # Requires React 19
  7. Customize Portable Text components

    main

    You can override default HTML rendering or provide components for custom content types by passing a components object to <PortableText />. The provided components are merged with defaults, so you only need to define what you want to change.

    Performance Note: To avoid unnecessary re-renders, ensure the components object maintains referential identity. Do not define it directly inside a component's render body; instead, define it outside the component or wrap it in useMemo.

    const myPortableTextComponents = {
      types: {
        image: ({value}) => <img src={value.imageUrl} />,
        callToAction: ({value, isInline}) =>
          isInline ? (
            <a href={value.url}>{value.text}</a>
          ) : (
            <div className="callToAction">{value.text}</div>
          ),
      },
    
      marks: {
        link: ({children, value}) => {
          const rel = !value.href.startsWith('/') ? 'noreferrer noopener' : undefined
          return (
            <a href={value.href} rel={rel}>
              {children}
            </a>
          )
        },
      },
    }
    
    const YourComponent = (props) => {
      return <PortableText value={props.value} components={myPortableTextComponents} />
    }
  8. Migrate from @sanity/block-content-to-react to @portabletext/react

    main

    If you are moving from the legacy @sanity/block-content-to-react package to @portabletext/react, you need to update your imports and prop names. The new package uses more standard React terminology and provides a better TypeScript experience.

    Required Changes:

    1. Import Name: BlockContent is now a named export called PortableText.
    2. Input Prop: blocks is renamed to value.
    3. Customization Prop: serializers is renamed to components.
    4. Component Props: Inside your custom components, node.value (for blocks) and mark.value (for marks) have been renamed to simply value.
    // From:
    import BlockContent from '@sanity/block-content-to-react'
    <BlockContent blocks={input} serializers={{ ... }} />
    
    // ✅ To:
    import { PortableText } from '@portabletext/react'
    <PortableText value={input} components={{ ... }} />
  9. Understand PortableTextComponentProps

    main

    Most custom components (blocks, types, marks) receive a standard set of props. The shape depends on whether the component is a block, a mark, or a custom type.

    Standard Component Props (PortableTextComponentProps<T>):

    • value: The raw JSON data for this node.
    • index: The index of the node within its parent.
    • isInline: Boolean indicating if the node is inline (child of a text span) or a block.
    • children: React child nodes.
    • renderNode: A function to render any node in the tree (rarely needed by users).

    Mark Component Props (PortableTextMarkComponentProps<M>):

    • value: The annotation data (if any).
    • text: The text content being marked.
    • markKey: A unique key for the mark.
    • markType: The name of the mark (e.g., em, link).
    • children: React child nodes.
  10. Customize rendering with PortableTextReactComponents

    main

    The PortableTextReactComponents interface defines the structure for the components prop. You can provide overrides for the following categories:

    • types: Renders custom object types (e.g., image, code_block). Use the _type value as the key.
    • marks: Renders inline annotations or decorators (e.g., link, strong).
    • block: Renders block-level elements (e.g., paragraph, h1). Can be an object mapping specific style values or a single component for all blocks.
    • list: Renders the container for a list (e.g., <ul>, <ol>).
    • listItem: Renders individual list items.
    • hardBreak: A component for \n characters. Defaults to <br />.
    • unknownMark, unknownType, unknownBlockStyle, unknownList, unknownListItem: Fallback components for unhandled types.
  11. Use InferComponents for type-safe component mapping

    main

    When using Sanity TypeGen, you can use InferComponents<T> to ensure your components object is perfectly typed against your queried data. This provides autocomplete for custom types, marks, and block styles.

    import { PortableText, type InferComponents } from '@portabletext/react'
    import { createClient } from '@sanity/client'
    import { defineQuery } from 'groq'
    
    const client = createClient({ ... })
    
    export default async function Page({ slug }: { slug: string }) {
      const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title, content}`)
      const data = await client.fetch(query, { slug })
    
      const components = {
        types: {
          // custom types are autocompleted and fully typed
        },
      } satisfies InferComponents<typeof data.content>
    
      return (
        <>
          {Array.isArray(data?.content) && (
            <PortableText components={components} value={data.content} />
          )}
        </>
      )
    }