@portabletext/react

Render Sanity's Portable Text as React components with full type-safe customization.

Library
npm
v8.0.1
377stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
82/100Excellent
Development Activity88
Maintenance92
Community64
Maturity56
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
82/100Excellent
Architecture88
Code Quality92
Innovation87
Learning Curve60

@portabletext/react turns Portable Text — the block-based rich content format used by Sanity and other structured-content platforms — into React elements. It ships default renderers for every standard block, mark, list, and text style in the spec, and lets you override any of them by passing a components object, without walking the block tree yourself.

The library works identically in Server and Client Components: it publishes a React Compiler-optimized build for client-side rendering and an uncompiled build behind the react-server export condition, so RSC output stays correct without extra configuration. Deep integration with Sanity TypeGen adds three inference utilities — InferValue, InferComponents, and InferStrictComponents — that derive the exact set of custom types, marks, and styles a component map needs to handle straight from your GROQ query results, catching missing handlers at compile time instead of a runtime console warning.

What You Get

  • Default components for every block style, mark, list, and list-item type defined in the Portable Text spec, so you can render before writing any custom UI.
  • A single components prop for overriding any renderer — merged with the defaults, so you only supply what you want to change.
  • Automatic dual builds for React Server Components and client components, selected through package export conditions with no configuration.
  • toPlainText() for extracting plain text from Portable Text, useful for meta descriptions, search indexes, or slug generation.
  • TypeGen-aware generic types (InferValue, InferComponents, InferStrictComponents) that derive component-map requirements directly from your Sanity queries.

Common Use Cases

  • Rendering blog post or page body content authored in Sanity Studio directly to React without a manual block-tree walker.
  • Building a reusable CustomPortableText wrapper component that TypeScript forces to stay in sync with the content schema via InferStrictComponents.
  • Rendering custom Portable Text object types like inline images, callouts, or embeds by supplying a types map.
  • Generating OpenGraph/meta descriptions or search-index text from rich content using toPlainText().

Under The Hood

Architecture The entry point (react-portable-text.tsx) exports a single PortableText function component. It normalizes the input into a block array, hands it to nestLists (from the sibling @portabletext/toolkit package) to group flat list-item blocks into a nested list tree, then dispatches each node through a recursive renderNode closure keyed on node kind — list, list item, span, custom type, block, text, or unknown. A merged component map (defaultComponents shallow-merged with any overrides via mergeComponents) is threaded through every branch, and a single handleMissingComponent callback centralizes fallback/warning behavior across all five node kinds. Mark/annotation nesting (e.g. links wrapping emphasis) is resolved in serializeBlock via buildMarksTree, also delegated to the toolkit package. The library holds no state and delegates all list/mark-nesting logic to @portabletext/toolkit, making it a thin, purely presentational rendering layer whose dispatch logic would need to change in lockstep with that sibling package’s tree-shaping abstractions.

Tech Stack Written almost entirely in TypeScript (94% of source bytes) with zero third-party runtime dependencies beyond the same-org @portabletext/toolkit and @portabletext/types packages, and React 19 as its sole peer dependency. Built with tsdown (a Rolldown-based bundler via the @sanity/tsdown-config preset) into separate types/react-server/default export conditions. Tooling favors oxlint (type-aware, deny-warnings) and oxfmt over ESLint/Prettier, vitest for unit tests, vite for the demo app, and changesets for versioned releases with auto-generated changelogs, all inside a pnpm workspace requiring Node >=22.12.

Code Quality An extensive vitest suite (portable-text, components, mutations, toPlainText) is backed by 60+ numbered fixture files covering individual spec edge cases — custom marks, list nesting, missing components, inline nodes. A dedicated test/next-app/ harness builds and runs a real Next.js app to verify the react-server export condition resolves correctly inside Server Components, a rigorous check for a rendering-condition concern that’s easy to silently regress. CI enforces both tsc —noEmit type-checking and type-aware oxlint with zero tolerance for warnings. Naming is consistent (Default*-prefixed defaults, unknown*-prefixed fallbacks) and every unrecognized-content path funnels through one centralized handleMissingComponent callback rather than throwing or silently dropping content.

API Design The public surface is minimal — one component and one helper — yet scales from zero-config JS usage up to fully typed, schema-enforced rendering via InferValue/InferComponents/InferStrictComponents, which derive component-map requirements directly from Sanity TypeGen query result types. InferStrictComponents specifically fails typechecking the moment the content schema gains a type without a matching renderer, a stronger contract than most rendering libraries offer. Overrides merge shallowly with the defaults so adopters only ever write the delta, and the configurable onMissingComponent hook routes unknown-content warnings into an app’s own observability pipeline instead of just the console. Documentation is unusually thorough for a small package, including a dedicated MIGRATING.md for adopters coming from the predecessor @sanity/block-content-to-react package.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search