Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions docs/01-app/02-guides/interactive-apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ Add `useOptimistic` with a reducer to remap the card's status on the current fra
'use client'

import { startTransition, use, useOptimistic } from 'react'
import { toast } from 'sonner'
import { updateStatus } from '@/features/task/task-actions'

export function Board({ tasksPromise }) {
Expand All @@ -449,7 +450,8 @@ export function Board({ tasksPromise }) {
function handleDrop(targetStatus, taskId) {
startTransition(async () => {
moveTask({ taskId, status: targetStatus })
await updateStatus(taskId, targetStatus)
const result = await updateStatus(taskId, targetStatus)
if (!result.success) toast.error(result.error)
})
}

Expand All @@ -467,9 +469,9 @@ export function Board({ tasksPromise }) {
}
```

The reducer maps over the task list and updates the status of the dragged card, leaving the rest unchanged. If a background refresh arrives mid-drag, for example from polling or another user's mutation, React re-runs the reducer with the updated base data so the optimistic move sits on top of fresh data.
The reducer maps over the task list and updates the status of the dragged card, leaving the rest unchanged. If a background refresh arrives mid-drag, for example from polling or another user's mutation, React re-runs the reducer with the updated base data so the optimistic move sits on top of fresh data. The Server Function returns an error result when the task no longer exists, which the client displays in a toast.

This step uses the standalone `startTransition` instead of `useTransition` because the hook's `isPending` would trigger the board fade from Step 3. The optimistic move already covers the visual feedback, so no pending indicator is needed. If the Server Function fails, the card reverts.
This step uses the standalone `startTransition` instead of `useTransition` because the hook's `isPending` would trigger the board fade from Step 3. The optimistic move already covers the visual feedback, so no pending indicator is needed. If the Server Function returns an expected error, the card reverts. Unexpected errors are forwarded to the nearest [error boundary](/docs/app/getting-started/error-handling).

A card dragged from "Todo" to "In Progress" now lands in the target column the moment you release.

Expand Down Expand Up @@ -724,11 +726,17 @@ The per-id tag `task-${id}` gives a single task its own handle. The broader `tas
'use server'

import { updateTag } from 'next/cache'
import { updateTaskStatus } from '@/lib/db'

export async function updateStatus(taskId: string, newStatus: Status) {
// …mutate
const updated = await updateTaskStatus(taskId, newStatus)
if (!updated) {
return { success: false as const, error: 'Task no longer exists' }
}

updateTag('tasks')
updateTag(`task-${taskId}`)
return { success: true as const, status: newStatus }
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ export const metadata = {
> **Good to know**:
>
> - `metadataBase` is typically set in root `app/layout.js` to apply to URL-based `metadata` fields across all routes.
> - When `generateMetadata` uses `'use cache'`, its return value must be serializable. `URL` instances are not supported by Cache Functions, so values such as `metadataBase` should be returned as strings (for example, `url.toString()`). See [`use cache` serialization requirements](/docs/app/api-reference/directives/use-cache#serialization).
> - All URL-based `metadata` fields that require absolute URLs can be configured with a `metadataBase` option.
> - `metadataBase` can contain a subdomain e.g. `https://app.acme.com` or base path e.g. `https://acme.com/start/from/here`
> - If a `metadata` field provides an absolute URL, `metadataBase` will be ignored.
Expand Down
13 changes: 8 additions & 5 deletions packages/next/src/build/adapter/build-complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { generateRoutesManifest } from '../generate-routes-manifest'
import { Bundler } from '../../lib/bundler'
import { resolveCacheHandlerPathToFilesystem } from '../../lib/format-dynamic-import-path'
import { InvariantError } from '../../shared/lib/invariant-error'
import type { __ApiPreviewProps } from '../../server/api-utils'

interface SharedRouteFields {
/**
Expand Down Expand Up @@ -605,6 +606,7 @@ export async function handleBuildComplete({
nextVersion,
hasStatic404,
hasStatic500,
previewProps,
routesManifest,
serverPropsPages,
hasNodeMiddleware,
Expand All @@ -629,6 +631,7 @@ export async function handleBuildComplete({
nextVersion: string
hasStatic404: boolean
hasStatic500: boolean
previewProps: __ApiPreviewProps
bundler: Bundler
staticPages: Set<string>
hasNodeMiddleware: boolean
Expand Down Expand Up @@ -847,7 +850,7 @@ export async function handleBuildComplete({
{
type: 'header',
key: 'x-prerender-revalidate',
value: prerenderManifest.preview.previewModeId,
value: previewProps.previewModeId,
},
],
}
Expand Down Expand Up @@ -1111,7 +1114,7 @@ export async function handleBuildComplete({
{
type: 'header',
key: 'x-prerender-revalidate',
value: prerenderManifest.preview.previewModeId,
value: previewProps.previewModeId,
},
],
}
Expand Down Expand Up @@ -1589,7 +1592,7 @@ export async function handleBuildComplete({
isAppPage && srcRoute !== '/_not-found'
? experimentalBypassFor
: undefined,
bypassToken: prerenderManifest.preview.previewModeId,
bypassToken: previewProps.previewModeId,
},
}
// Classification describes the primary HTML or Route Handler body,
Expand Down Expand Up @@ -1861,7 +1864,7 @@ export async function handleBuildComplete({
renderingMode,
partialFallback: canEmitPartialFallback || undefined,
bypassFor: isAppPage ? experimentalBypassFor : undefined,
bypassToken: prerenderManifest.preview.previewModeId,
bypassToken: previewProps.previewModeId,
},
}

Expand Down Expand Up @@ -2089,7 +2092,7 @@ export async function handleBuildComplete({
{
type: 'cookie',
key: '__prerender_bypass',
value: prerenderManifest.preview.previewModeId,
value: previewProps.previewModeId,
},
{
type: 'cookie',
Expand Down
12 changes: 11 additions & 1 deletion packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
FUNCTIONS_CONFIG_MANIFEST,
DYNAMIC_CSS_MANIFEST,
TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST,
PREVIEW_PROPS_MANIFEST,
} from '../shared/lib/constants'
import {
UNDERSCORE_NOT_FOUND_ROUTE,
Expand Down Expand Up @@ -414,6 +415,7 @@ export type PrerenderManifest = {
routes: { [route: string]: PrerenderManifestRoute }
dynamicRoutes: { [route: string]: DynamicPrerenderManifestRoute }
notFoundRoutes: string[]
/** @deprecated only kept for the builder, use PreviewPropsManifest within Next.js itself */
preview: __ApiPreviewProps
}

Expand Down Expand Up @@ -507,6 +509,7 @@ function getPagesFallbackClassification(
}

export type SubresourceIntegrityManifest = Record<string, string>
export type PreviewPropsManifest = __ApiPreviewProps

type ManifestBuiltRoute = {
/**
Expand Down Expand Up @@ -2012,6 +2015,7 @@ export default async function build(
path.relative(distDir, pagesManifestPath),
BUILD_MANIFEST,
PRERENDER_MANIFEST,
path.join(SERVER_DIRECTORY, PREVIEW_PROPS_MANIFEST),
path.join(SERVER_DIRECTORY, FUNCTIONS_CONFIG_MANIFEST),
path.join(SERVER_DIRECTORY, MIDDLEWARE_MANIFEST),
path.join(SERVER_DIRECTORY, MIDDLEWARE_BUILD_MANIFEST + '.js'),
Expand Down Expand Up @@ -4414,11 +4418,16 @@ export default async function build(
version: 4,
routes: {},
dynamicRoutes: {},
preview: previewProps,
notFoundRoutes: [],
preview: previewProps,
})
}

await writeManifest(
path.join(distDir, 'server', PREVIEW_PROPS_MANIFEST),
previewProps
)

// #endregion

await writeImagesManifest(distDir, config)
Expand Down Expand Up @@ -4549,6 +4558,7 @@ export default async function build(
outputFileTracingRoot,
hasNodeMiddleware,
hasInstrumentationHook,
previewProps,
adapterPath,
pageKeys: pageKeys.pages,
appPageKeys: emittedAppPageKeys,
Expand Down
6 changes: 5 additions & 1 deletion packages/next/src/build/templates/app-page-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export function createAppPageEntrypoint({
interceptionRoutePatterns,
deploymentId,
clientAssetToken,
previewProps,
} = prepareResult

let { isOnDemandRevalidate } = prepareResult
Expand Down Expand Up @@ -804,6 +805,7 @@ export function createAppPageEntrypoint({
(await routeModule.getIncrementalCache(
req,
nextConfig,
previewProps,
prerenderManifest,
isMinimalMode
))
Expand Down Expand Up @@ -890,7 +892,7 @@ export function createAppPageEntrypoint({
crossOrigin: nextConfig.crossOrigin,
trailingSlash: nextConfig.trailingSlash,
images: nextConfig.images,
previewProps: prerenderManifest.preview,
previewProps,
enableTainting: nextConfig.experimental.taint,
reactMaxHeadersLength: nextConfig.reactMaxHeadersLength,

Expand Down Expand Up @@ -1240,6 +1242,7 @@ export function createAppPageEntrypoint({
nextConfig,
routeKind: RouteKind.APP_PAGE,
isFallback: true,
previewProps,
prerenderManifest,
isRoutePPREnabled,
responseGenerator: async () =>
Expand Down Expand Up @@ -1610,6 +1613,7 @@ export function createAppPageEntrypoint({
isRoutePPREnabled,
req,
nextConfig,
previewProps,
prerenderManifest,
waitUntil: ctx.waitUntil,
isMinimalMode,
Expand Down
5 changes: 4 additions & 1 deletion packages/next/src/build/templates/app-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export async function handler(
resolvedPathname,
clientReferenceManifest,
serverActionsManifest,
previewProps,
} = prepareResult

const normalizedSrcPage = normalizeAppPath(srcPage)
Expand Down Expand Up @@ -221,6 +222,7 @@ export async function handler(
(await routeModule.getIncrementalCache(
req,
nextConfig,
previewProps,
prerenderManifest,
isMinimalMode
))
Expand All @@ -230,7 +232,7 @@ export async function handler(

const context: AppRouteRouteHandlerContext = {
params,
previewProps: prerenderManifest.preview,
previewProps,
renderOpts: {
experimental: {
authInterrupts: Boolean(nextConfig.experimental.authInterrupts),
Expand Down Expand Up @@ -401,6 +403,7 @@ export async function handler(
cacheKey,
routeKind: RouteKind.APP_ROUTE,
isFallback: false,
previewProps,
prerenderManifest,
isRoutePPREnabled: false,
isOnDemandRevalidate,
Expand Down
6 changes: 4 additions & 2 deletions packages/next/src/build/templates/edge-ssr-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ async function requestHandler(
nextConfig,
buildManifest,
prerenderManifest,
previewProps,
reactLoadableManifest,
subresourceIntegrityManifest,
dynamicCssManifest,
Expand All @@ -102,7 +103,7 @@ async function requestHandler(
const botType = getBotType(req.headers.get('User-Agent') || '')
const { isOnDemandRevalidate } = checkIsOnDemandRevalidate(
req.headers,
prerenderManifest.preview
previewProps
)

const closeController = new CloseController()
Expand Down Expand Up @@ -149,7 +150,7 @@ async function requestHandler(
crossOrigin: nextConfig.crossOrigin,
trailingSlash: nextConfig.trailingSlash,
images: nextConfig.images,
previewProps: prerenderManifest.preview,
previewProps: previewProps,
enableTainting: nextConfig.experimental.taint,
reactMaxHeadersLength: nextConfig.reactMaxHeadersLength,

Expand Down Expand Up @@ -195,6 +196,7 @@ async function requestHandler(
incrementalCache: await pageRouteModule.getIncrementalCache(
baseReq,
nextConfig,
previewProps,
prerenderManifest,
true
),
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/build/templates/edge-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ async function requestHandler(
deploymentId,
isNextDataRequest,
buildManifest,
prerenderManifest,
previewProps,
reactLoadableManifest,
subresourceIntegrityManifest,
dynamicCssManifest,
Expand Down Expand Up @@ -155,7 +155,7 @@ async function requestHandler(
ComponentMod: pageMod,
pageConfig: pageMod.pageConfig,
routeModule: pageMod.routeModule,
previewProps: prerenderManifest.preview,
previewProps,
basePath: nextConfig.basePath,
assetPrefix: nextConfig.assetPrefix,
images: nextConfig.images,
Expand Down
5 changes: 2 additions & 3 deletions packages/next/src/build/templates/pages-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ export async function handler(
return
}

const { query, params, prerenderManifest, routerServerContext } =
prepareResult
const { query, params, previewProps, routerServerContext } = prepareResult

try {
const method = req.method || 'GET'
Expand Down Expand Up @@ -106,7 +105,7 @@ export async function handler(
.__NEXT_TRUST_HOST_HEADER as any as boolean,
// TODO: get this from from runtime env so manifest
// doesn't need to load
previewProps: prerenderManifest.preview,
previewProps,
propagateError: false,
dev: routeModule.isDev,
page: 'VAR_DEFINITION_PAGE',
Expand Down
16 changes: 9 additions & 7 deletions packages/next/src/export/helpers/create-incremental-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,25 @@ export async function createIncrementalCache({
}
}

let previewProps = {
previewModeEncryptionKey: '',
previewModeId: '',
previewModeSigningKey: '',
}
const incrementalCache = new IncrementalCache({
dev: false,
requestHeaders: requestHeaders || {},
flushToDisk,
maxMemoryCacheSize: cacheMaxMemorySize,
fetchCacheKeyPrefix,
getPrerenderManifest: () => ({
previewProps,
prerenderManifest: {
version: 4,
routes: {},
dynamicRoutes: {},
preview: {
previewModeEncryptionKey: '',
previewModeId: '',
previewModeSigningKey: '',
},
notFoundRoutes: [],
}),
preview: previewProps,
},
fs: nodeFs,
serverDistDir: path.join(distDir, 'server'),
CurCacheHandler: CacheHandler,
Expand Down
9 changes: 8 additions & 1 deletion packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
import {
createStaticWorker,
type PrerenderManifest,
type PreviewPropsManifest,
type StaticWorker,
} from '../build'
import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
Expand Down Expand Up @@ -43,6 +44,7 @@ import {
APP_PATH_ROUTES_MANIFEST,
ROUTES_MANIFEST,
FUNCTIONS_CONFIG_MANIFEST,
PREVIEW_PROPS_MANIFEST,
} from '../shared/lib/constants'
import loadConfig from '../server/config'
import type { ExportPathMap } from '../server/config-shared'
Expand Down Expand Up @@ -260,6 +262,11 @@ async function exportAppImpl(
!options.pages &&
(require(join(distDir, SERVER_DIRECTORY, PAGES_MANIFEST)) as PagesManifest)

let previewProps: DeepReadonly<PreviewPropsManifest> | undefined
try {
previewProps = require(join(distDir, 'server', PREVIEW_PROPS_MANIFEST))
} catch {}

let prerenderManifest: DeepReadonly<PrerenderManifest> | undefined
try {
prerenderManifest = require(join(distDir, PRERENDER_MANIFEST))
Expand Down Expand Up @@ -476,7 +483,7 @@ async function exportAppImpl(

// Start the rendering process
const renderOpts: WorkerRenderOptsPartial = {
previewProps: prerenderManifest?.preview,
previewProps,
isBuildTimePrerendering: true,
assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''),
distDir,
Expand Down
Loading
Loading