Skip to content

Commit 874f775

Browse files
Version Packages (#334)
Co-authored-by: ecoscript[bot] <143032650+ecoscript[bot]@users.noreply.github.com>
1 parent 87a9b1e commit 874f775

3 files changed

Lines changed: 147 additions & 118 deletions

File tree

.changeset/react-server-dual-build.md

Lines changed: 0 additions & 17 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 146 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# @portabletext/react
22

3+
## 7.0.0
4+
5+
### Major Changes
6+
7+
- [#333](https://github.com/portabletext/react-portabletext/pull/333) [`87a9b1e`](https://github.com/portabletext/react-portabletext/commit/87a9b1ec0a83638ad523d53dd41501333281fb93) Thanks [@stipsan](https://github.com/stipsan)! - `<PortableText>` now ships as two builds selected by export conditions: the `default` build is optimized with [React Compiler](https://react.dev/learn/react-compiler) auto-memoization, and an uncompiled build serves the `react-server` condition, since React Server Components refuse to load compiled output ([facebook/react#31702](https://github.com/facebook/react/issues/31702)):
8+
9+
```json
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"react-server": "./dist/index.react-server.js",
13+
"default": "./dist/index.js"
14+
}
15+
```
16+
17+
There are no API changes. Client components and SSR get the compiled build, with finer-grained memoization than the manual `useMemo` calls it replaces, and server components render the same source uncompiled — they render exactly once, so memoization can never pay off there.
18+
19+
**BREAKING**: React 19 is now required (`peerDependencies.react` is `^19`) — the compiled build loads `react/compiler-runtime`, which only exists in React 19. Stay on `@portabletext/react@6` if you are on React 18.
20+
321
## 6.2.0
422

523
### Minor Changes
@@ -9,6 +27,7 @@
927
`<PortableText>` now infers the shape of every component handler from the `value` prop. When you pass a value typed by [Sanity TypeGen](https://www.sanity.io/docs/apis-and-sdks/sanity-typegen), `components.types`, `components.marks`, `components.block`, `components.list`, and `components.listItem` all receive precise `value` props for the exact content the query returned.
1028

1129
Three new utility types ship with this feature:
30+
1231
- `InferComponents<T>` - same inference as the inline `components` prop, for hoisting components out of JSX.
1332
- `InferStrictComponents<T>` - strict variant that requires a handler for every inferred custom type, mark, block style, and list style, and rejects handlers that aren't in the schema (and therefore not visible to TypeGen).
1433
- `InferValue<T>` - derives a Portable Text array value type from any TypeGen query result type, useful for re-usable wrapper components.
@@ -19,36 +38,41 @@
1938

2039
```ts
2140
// sanity.config.ts
22-
import {defineArrayMember, defineConfig, defineField, defineType} from 'sanity'
41+
import {
42+
defineArrayMember,
43+
defineConfig,
44+
defineField,
45+
defineType,
46+
} from "sanity";
2347

2448
export default defineConfig({
25-
name: 'default',
26-
projectId: 'abc123',
27-
dataset: 'production',
49+
name: "default",
50+
projectId: "abc123",
51+
dataset: "production",
2852
schema: {
2953
types: [
3054
defineType({
31-
name: 'post',
32-
type: 'document',
55+
name: "post",
56+
type: "document",
3357
fields: [
34-
defineField({name: 'title', type: 'string'}),
58+
defineField({ name: "title", type: "string" }),
3559
defineField({
36-
name: 'content',
37-
type: 'array',
60+
name: "content",
61+
type: "array",
3862
of: [
39-
defineArrayMember({type: 'block'}),
63+
defineArrayMember({ type: "block" }),
4064
defineArrayMember({
41-
type: 'image',
42-
options: {hotspot: true},
43-
fields: [defineField({name: 'alt', type: 'string'})],
65+
type: "image",
66+
options: { hotspot: true },
67+
fields: [defineField({ name: "alt", type: "string" })],
4468
}),
4569
],
4670
}),
4771
],
4872
}),
4973
],
5074
},
51-
})
75+
});
5276
```
5377

5478
#### Before: hand-typing handlers
@@ -57,24 +81,26 @@
5781

5882
```tsx
5983
// app/[slug]/page.tsx
60-
import {createClient} from '@sanity/client'
61-
import {createImageUrlBuilder} from '@sanity/image-url'
62-
import {PortableText} from '@portabletext/react'
63-
import {defineQuery} from 'groq'
84+
import { createClient } from "@sanity/client";
85+
import { createImageUrlBuilder } from "@sanity/image-url";
86+
import { PortableText } from "@portabletext/react";
87+
import { defineQuery } from "groq";
6488

6589
const client = createClient({
66-
projectId: 'abc123',
67-
dataset: 'production',
90+
projectId: "abc123",
91+
dataset: "production",
6892
useCdn: true,
69-
apiVersion: '2026-05-04',
70-
})
71-
const builder = createImageUrlBuilder(client)
93+
apiVersion: "2026-05-04",
94+
});
95+
const builder = createImageUrlBuilder(client);
7296

73-
export default async function Page({slug}: {slug: string}) {
74-
const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`)
75-
const data = await client.fetch(query, {slug})
97+
export default async function Page({ slug }: { slug: string }) {
98+
const query = defineQuery(
99+
`*[_type == "post" && slug.current == $slug][0]{title,content}`
100+
);
101+
const data = await client.fetch(query, { slug });
76102

77-
if (!data) return notFound()
103+
if (!data) return notFound();
78104

79105
return (
80106
<article>
@@ -87,35 +113,37 @@
87113
}: {
88114
value: {
89115
asset?: {
90-
_ref: string
91-
_type: 'reference'
92-
_weak?: boolean
93-
}
116+
_ref: string;
117+
_type: "reference";
118+
_weak?: boolean;
119+
};
94120
hotspot?: {
95-
_type: 'sanity.imageHotspot'
96-
x?: number
97-
y?: number
98-
height?: number
99-
width?: number
100-
}
121+
_type: "sanity.imageHotspot";
122+
x?: number;
123+
y?: number;
124+
height?: number;
125+
width?: number;
126+
};
101127
crop?: {
102-
_type: 'sanity.imageCrop'
103-
top?: number
104-
bottom?: number
105-
left?: number
106-
right?: number
107-
}
108-
alt?: string
109-
_type: 'image'
110-
_key: string
111-
}
112-
}) => <img src={builder.image(value).url()} alt={value.alt || ''} />,
128+
_type: "sanity.imageCrop";
129+
top?: number;
130+
bottom?: number;
131+
left?: number;
132+
right?: number;
133+
};
134+
alt?: string;
135+
_type: "image";
136+
_key: string;
137+
};
138+
}) => (
139+
<img src={builder.image(value).url()} alt={value.alt || ""} />
140+
),
113141
},
114142
}}
115143
value={data.content}
116144
/>
117145
</article>
118-
)
146+
);
119147
}
120148
```
121149

@@ -125,24 +153,26 @@
125153

126154
```tsx
127155
// app/[slug]/page.tsx
128-
import {createClient} from '@sanity/client'
129-
import {createImageUrlBuilder} from '@sanity/image-url'
130-
import {PortableText} from '@portabletext/react'
131-
import {defineQuery} from 'groq'
156+
import { createClient } from "@sanity/client";
157+
import { createImageUrlBuilder } from "@sanity/image-url";
158+
import { PortableText } from "@portabletext/react";
159+
import { defineQuery } from "groq";
132160

133161
const client = createClient({
134-
projectId: 'abc123',
135-
dataset: 'production',
162+
projectId: "abc123",
163+
dataset: "production",
136164
useCdn: true,
137-
apiVersion: '2026-05-04',
138-
})
139-
const builder = createImageUrlBuilder(client)
165+
apiVersion: "2026-05-04",
166+
});
167+
const builder = createImageUrlBuilder(client);
140168

141-
export default async function Page({slug}: {slug: string}) {
142-
const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`)
143-
const data = await client.fetch(query, {slug})
169+
export default async function Page({ slug }: { slug: string }) {
170+
const query = defineQuery(
171+
`*[_type == "post" && slug.current == $slug][0]{title,content}`
172+
);
173+
const data = await client.fetch(query, { slug });
144174

145-
if (!data) return notFound()
175+
if (!data) return notFound();
146176

147177
return (
148178
<article>
@@ -151,13 +181,15 @@
151181
components={{
152182
types: {
153183
// value is fully typed from the query result, no annotation needed
154-
image: ({value}) => <img src={builder.image(value).url()} alt={value.alt || ''} />,
184+
image: ({ value }) => (
185+
<img src={builder.image(value).url()} alt={value.alt || ""} />
186+
),
155187
},
156188
}}
157189
value={data.content}
158190
/>
159191
</article>
160-
)
192+
);
161193
}
162194
```
163195

@@ -167,37 +199,41 @@
167199

168200
```tsx
169201
// app/[slug]/page.tsx
170-
import {createClient} from '@sanity/client'
171-
import {createImageUrlBuilder} from '@sanity/image-url'
172-
import {PortableText, type InferComponents} from '@portabletext/react'
173-
import {defineQuery} from 'groq'
202+
import { createClient } from "@sanity/client";
203+
import { createImageUrlBuilder } from "@sanity/image-url";
204+
import { PortableText, type InferComponents } from "@portabletext/react";
205+
import { defineQuery } from "groq";
174206

175207
const client = createClient({
176-
projectId: 'abc123',
177-
dataset: 'production',
208+
projectId: "abc123",
209+
dataset: "production",
178210
useCdn: true,
179-
apiVersion: '2026-05-04',
180-
})
181-
const builder = createImageUrlBuilder(client)
211+
apiVersion: "2026-05-04",
212+
});
213+
const builder = createImageUrlBuilder(client);
182214

183-
export default async function Page({slug}: {slug: string}) {
184-
const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`)
185-
const data = await client.fetch(query, {slug})
215+
export default async function Page({ slug }: { slug: string }) {
216+
const query = defineQuery(
217+
`*[_type == "post" && slug.current == $slug][0]{title,content}`
218+
);
219+
const data = await client.fetch(query, { slug });
186220

187-
if (!data) return notFound()
221+
if (!data) return notFound();
188222

189223
const components = {
190224
types: {
191-
image: ({value}) => <img src={builder.image(value).url()} alt={value.alt || ''} />,
225+
image: ({ value }) => (
226+
<img src={builder.image(value).url()} alt={value.alt || ""} />
227+
),
192228
},
193-
} satisfies InferComponents<typeof data.content>
229+
} satisfies InferComponents<typeof data.content>;
194230

195231
return (
196232
<article>
197233
<h1>{data.title}</h1>
198234
<PortableText components={components} value={data.content} />
199235
</article>
200-
)
236+
);
201237
}
202238
```
203239

@@ -207,46 +243,56 @@
207243

208244
```tsx
209245
// app/[slug]/page.tsx
210-
import {createClient, type SanityQueries} from '@sanity/client'
211-
import {createImageUrlBuilder} from '@sanity/image-url'
212-
import {PortableText, type InferStrictComponents, type InferValue} from '@portabletext/react'
213-
import {defineQuery} from 'groq'
246+
import { createClient, type SanityQueries } from "@sanity/client";
247+
import { createImageUrlBuilder } from "@sanity/image-url";
248+
import {
249+
PortableText,
250+
type InferStrictComponents,
251+
type InferValue,
252+
} from "@portabletext/react";
253+
import { defineQuery } from "groq";
214254

215255
const client = createClient({
216-
projectId: 'abc123',
217-
dataset: 'production',
256+
projectId: "abc123",
257+
dataset: "production",
218258
useCdn: true,
219-
apiVersion: '2026-05-04',
220-
})
221-
const builder = createImageUrlBuilder(client)
259+
apiVersion: "2026-05-04",
260+
});
261+
const builder = createImageUrlBuilder(client);
222262

223263
// Array value type for every Portable Text item shape across all registered queries.
224-
type PortableTextValue = InferValue<SanityQueries[keyof SanityQueries]>
264+
type PortableTextValue = InferValue<SanityQueries[keyof SanityQueries]>;
225265

226-
function CustomPortableText({value}: {value: PortableTextValue}) {
266+
function CustomPortableText({ value }: { value: PortableTextValue }) {
227267
const components = {
228268
types: {
229-
image: ({value}) => <img src={builder.image(value).url()} alt={value.alt || ''} />,
269+
image: ({ value }) => (
270+
<img src={builder.image(value).url()} alt={value.alt || ""} />
271+
),
230272
},
231-
} satisfies InferStrictComponents<PortableTextValue>
273+
} satisfies InferStrictComponents<PortableTextValue>;
232274
// ^ TypeScript errors when the schema gains a custom type, mark, or list
233275
// style without a matching handler defined here
234276

235-
return <PortableText components={components} value={value} />
277+
return <PortableText components={components} value={value} />;
236278
}
237279

238-
export default async function Page({slug}: {slug: string}) {
239-
const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`)
240-
const data = await client.fetch(query, {slug})
280+
export default async function Page({ slug }: { slug: string }) {
281+
const query = defineQuery(
282+
`*[_type == "post" && slug.current == $slug][0]{title,content}`
283+
);
284+
const data = await client.fetch(query, { slug });
241285

242-
if (!data) return notFound()
286+
if (!data) return notFound();
243287

244288
return (
245289
<article>
246290
<h1>{data.title}</h1>
247-
{Array.isArray(data.content) && <CustomPortableText value={data.content} />}
291+
{Array.isArray(data.content) && (
292+
<CustomPortableText value={data.content} />
293+
)}
248294
</article>
249-
)
295+
);
250296
}
251297
```
252298

0 commit comments

Comments
 (0)