3.5 KiB
3.5 KiB
title, description, tags
| title | description | tags | |||||
|---|---|---|---|---|---|---|---|
| Serialize Portable Text to React | Render Portable Text in React and Next.js using @portabletext/react |
|
Serialize Portable Text to React
Use @portabletext/react (or re-exported from next-sanity) to render PT in React/Next.js.
npm install @portabletext/react
Basic Usage
import {PortableText} from '@portabletext/react'
// or: import {PortableText} from 'next-sanity'
export function Body({value}: {value: PortableTextBlock[]}) {
return <PortableText value={value} components={components} />
}
Typed Components Object
import type {PortableTextComponents} from '@portabletext/react'
const components: PortableTextComponents = {
// Block styles
block: {
h1: ({children}) => <h1 className="text-4xl font-bold">{children}</h1>,
h2: ({children}) => <h2 className="text-3xl font-semibold">{children}</h2>,
blockquote: ({children}) => (
<blockquote className="border-l-4 pl-4 italic">{children}</blockquote>
),
// 'normal' is the default paragraph style
},
// Custom block types
types: {
image: ({value}) => (
<img
src={urlFor(value).width(800).url()}
alt={value.alt || ''}
loading="lazy"
/>
),
code: ({value}) => (
<pre data-language={value.language}>
<code>{value.code}</code>
</pre>
),
},
// Marks (decorators + annotations)
marks: {
// Decorator
highlight: ({children}) => (
<span className="bg-yellow-200">{children}</span>
),
// Annotation
link: ({children, value}) => {
const rel = !value?.href?.startsWith('/') ? 'noreferrer noopener' : undefined
return (
<a href={value?.href} rel={rel}>
{children}
</a>
)
},
internalLink: ({children, value}) => (
<a href={`/${value?.slug}`}>{children}</a>
),
},
// Lists
list: {
bullet: ({children}) => <ul className="list-disc ml-6">{children}</ul>,
number: ({children}) => <ol className="list-decimal ml-6">{children}</ol>,
},
listItem: {
bullet: ({children}) => <li>{children}</li>,
},
}
Props Reference
| Component type | Props received |
|---|---|
block.* |
{children, value} — value is the full block |
types.* |
{value, isInline} — value is the custom block data |
marks.* |
{children, value, markType, markKey} — value is the markDef data |
list.* |
{children, value} |
listItem.* |
{children, value} |
Performance: Stabilize the Components Object
❌ Bad — recreated every render:
function Body({value}) {
return <PortableText value={value} components={{
types: {image: ({value}) => <img src={value.url} />}
}} />
}
✅ Good — defined outside or memoized:
const components: PortableTextComponents = {
types: {image: ({value}) => <img src={value.url} />}
}
function Body({value}) {
return <PortableText value={value} components={components} />
}
Plain Text Extraction
import {toPlainText} from '@portabletext/react'
const text = toPlainText(blocks) // for meta descriptions, search indexing
Tailwind Typography Shortcut
For simple blogs without custom blocks, wrap in prose:
<article className="prose lg:prose-xl">
<PortableText value={value} />
</article>