diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index cfaafe8ad8ba1..b6f903eac1849 100644 --- a/crates/next-api/src/app.rs +++ b/crates/next-api/src/app.rs @@ -1566,8 +1566,6 @@ impl AppEndpoint { if emit_manifests == EmitManifests::Full { let dynamic_import_entries = collect_next_dynamic_chunks( - *module_graphs.full, - *client_chunking_context, next_dynamic_imports, NextDynamicChunkAvailability::ClientReferences( &*(client_references_chunks.await?), @@ -1682,8 +1680,6 @@ impl AppEndpoint { let loadable_manifest_output = if emit_manifests == EmitManifests::Full { // create react-loadable-manifest for next/dynamic let dynamic_import_entries = collect_next_dynamic_chunks( - *module_graphs.full, - *client_chunking_context, next_dynamic_imports, NextDynamicChunkAvailability::ClientReferences( &*(client_references_chunks.await?), diff --git a/crates/next-api/src/dynamic_imports.rs b/crates/next-api/src/dynamic_imports.rs index dad3bfdc4ad16..87a48d80ddd92 100644 --- a/crates/next-api/src/dynamic_imports.rs +++ b/crates/next-api/src/dynamic_imports.rs @@ -30,24 +30,29 @@ use turbo_tasks::{ debug::ValueDebugFormat, trace::TraceRawVcs, }; use turbopack_core::{ - chunk::{ChunkableModule, ChunkingContext, availability_info::AvailabilityInfo}, + chunk::{ChunkGroupResult, ChunkableModule}, module::Module, - module_graph::{ModuleGraph, ModuleGraphLayer}, + module_graph::ModuleGraphLayer, output::{OutputAssetsReference, OutputAssetsWithReferenced}, }; use crate::module_graph::DynamicImportEntriesWithImporter; pub(crate) enum NextDynamicChunkAvailability<'a> { - /// In App Router, the client references + /// In App Router, the client references chunks contain the async loaders ClientReferences(&'a ClientReferencesChunks), - /// In Pages Router, the base page chunk group - AvailabilityInfo(AvailabilityInfo), + /// In Pages Router, the base page chunk group result + PageChunkGroup(&'a ChunkGroupResult), } +/// Collects the chunk outputs for next/dynamic imports by looking up pre-computed +/// async loaders from the chunk group results. +/// +/// This function no longer recomputes chunks - instead it looks up the async loader +/// outputs that were already computed by `make_chunk_group` when the parent chunk +/// groups were created. This ensures consistency between the manifest and the actual +/// chunks served at runtime. pub(crate) async fn collect_next_dynamic_chunks( - module_graph: Vc, - chunking_context: Vc>, dynamic_import_entries: ReadRef, chunking_availability: NextDynamicChunkAvailability<'_>, ) -> Result> { @@ -57,28 +62,36 @@ pub(crate) async fn collect_next_dynamic_chunks( .map(|(dynamic_entry, parent_client_reference)| async move { let module = ResolvedVc::upcast::>(*dynamic_entry); - // This is the availability info for the parent chunk group, i.e. the client reference - // containing the next/dynamic imports - let availability_info = match chunking_availability { + // Look up the pre-computed async loader from the parent chunk group + let async_loader = match chunking_availability { NextDynamicChunkAvailability::ClientReferences(client_reference_chunks) => { - client_reference_chunks + // For App Router: look up the chunk group for the parent client reference, + // then find the async loader for this dynamic entry + let parent_ref = parent_client_reference + .context("Parent client reference not found for next/dynamic import")?; + let chunk_group = client_reference_chunks .client_component_client_chunks - .get( - &parent_client_reference.context( - "Parent client reference not found for next/dynamic import", - )?, - ) + .get(&parent_ref) .context("Client reference chunk group not found for next/dynamic import")? - .await? - .availability_info + .await?; + // Copy the ResolvedVc out of the map to avoid lifetime issues + *chunk_group.async_loaders_by_module.get(&module).context( + "Dynamic entry not found in async loaders - this may indicate the dynamic \ + import is not reachable from the client reference", + )? } - NextDynamicChunkAvailability::AvailabilityInfo(availability_info) => { - *availability_info + NextDynamicChunkAvailability::PageChunkGroup(chunk_group) => { + // For Pages Router: look up directly in the page's chunk group + // Copy the ResolvedVc out of the map to avoid lifetime issues + *chunk_group.async_loaders_by_module.get(&module).context( + "Dynamic entry not found in async loaders - this may indicate the dynamic \ + import is not reachable from the page entry", + )? } }; - let async_loader = - chunking_context.async_loader_chunk_item(*module, module_graph, availability_info); + // Get the output assets from the async loader reference + // Upcast to OutputAssetsReference to call references() let async_chunk_group = async_loader.references().to_resolved().await?; Ok((*dynamic_entry, (*dynamic_entry, async_chunk_group))) diff --git a/crates/next-api/src/pages.rs b/crates/next-api/src/pages.rs index ecfa167d27a3a..be0f68c0cb910 100644 --- a/crates/next-api/src/pages.rs +++ b/crates/next-api/src/pages.rs @@ -927,7 +927,7 @@ impl PageEndpoint { let ssr_module_graph = self.ssr_module_graph(); let next_dynamic_imports = if let PageEndpointType::Html = this.ty { - let client_availability_info = self.client_chunk_group().await?.availability_info; + let client_chunk_group = self.client_chunk_group().await?; let client_module_graph = self.client_module_graph(); let per_page_module_graph = *project.per_page_module_graph().await?; @@ -966,26 +966,21 @@ impl PageEndpoint { NextDynamicGraphs::new(client_module_graph, per_page_module_graph) .get_next_dynamic_imports_for_endpoint(self.client_module()) .await?; - Some((next_dynamic_imports, client_availability_info)) + Some((next_dynamic_imports, client_chunk_group)) } else { None }; - let dynamic_import_entries = if let Some(( - next_dynamic_imports, - client_availability_info, - )) = next_dynamic_imports - { - collect_next_dynamic_chunks( - self.client_module_graph(), - project.client_chunking_context(), - next_dynamic_imports, - NextDynamicChunkAvailability::AvailabilityInfo(client_availability_info), - ) - .await? - } else { - DynamicImportedChunks::default().resolved_cell() - }; + let dynamic_import_entries = + if let Some((next_dynamic_imports, client_chunk_group)) = next_dynamic_imports { + collect_next_dynamic_chunks( + next_dynamic_imports, + NextDynamicChunkAvailability::PageChunkGroup(&client_chunk_group), + ) + .await? + } else { + DynamicImportedChunks::default().resolved_cell() + }; let chunking_context: Vc> = match runtime { NextRuntime::NodeJs => Vc::upcast(node_chunking_context), diff --git a/crates/next-core/src/next_app/app_client_references_chunks.rs b/crates/next-core/src/next_app/app_client_references_chunks.rs index 21b2e04dfffd9..bc8113106dcfe 100644 --- a/crates/next-core/src/next_app/app_client_references_chunks.rs +++ b/crates/next-core/src/next_app/app_client_references_chunks.rs @@ -164,8 +164,8 @@ pub async fn get_app_client_references_chunks( let mut current_client_chunk_group = ChunkGroupResult { assets: ResolvedVc::cell(vec![]), referenced_assets: ResolvedVc::cell(vec![]), - references: ResolvedVc::cell(vec![]), availability_info: client_availability_info, + async_loaders_by_module: FxIndexMap::default(), } .resolved_cell(); let mut current_ssr_chunk_group = ChunkGroupResult::empty_resolved(); diff --git a/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs b/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs index 8afd9b5af6729..a0d624912d963 100644 --- a/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs +++ b/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs @@ -6,7 +6,7 @@ use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{IntoTraitRef, ResolvedVc, ValueToString, Vc}; use turbo_tasks_fs::{File, FileContent}; use turbopack_core::{ - asset::{Asset, AssetContent}, + asset::AssetContent, chunk::{ AsyncModuleInfo, ChunkGroupType, ChunkItem, ChunkType, ChunkableModule, ChunkableModuleReference, ChunkingContext, ChunkingType, ChunkingTypeOption, @@ -249,18 +249,6 @@ impl Module for EcmascriptClientReferenceModule { } } -#[turbo_tasks::value_impl] -impl Asset for EcmascriptClientReferenceModule { - #[turbo_tasks::function] - fn content(&self) -> Vc { - AssetContent::File( - FileContent::Content("// This is a proxy module for Next.js client references.".into()) - .resolved_cell(), - ) - .cell() - } -} - #[turbo_tasks::value_impl] impl ChunkableModule for EcmascriptClientReferenceModule { #[turbo_tasks::function] diff --git a/crates/next-core/src/next_dynamic/dynamic_module.rs b/crates/next-core/src/next_dynamic/dynamic_module.rs index 07a0c3b6b1225..3ff31f98e31a5 100644 --- a/crates/next-core/src/next_dynamic/dynamic_module.rs +++ b/crates/next-core/src/next_dynamic/dynamic_module.rs @@ -4,9 +4,7 @@ use anyhow::Result; use indoc::formatdoc; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ResolvedVc, Vc}; -use turbo_tasks_fs::FileContent; use turbopack_core::{ - asset::{Asset, AssetContent}, chunk::{ChunkItem, ChunkType, ChunkableModule, ChunkingContext, ModuleChunkItemIdExt}, ident::AssetIdent, module::{Module, ModuleSideEffects}, @@ -78,18 +76,6 @@ impl Module for NextDynamicEntryModule { } } -#[turbo_tasks::value_impl] -impl Asset for NextDynamicEntryModule { - #[turbo_tasks::function] - fn content(&self) -> Vc { - AssetContent::File( - FileContent::Content("// This is a marker module for Next.js dynamic.".into()) - .resolved_cell(), - ) - .cell() - } -} - #[turbo_tasks::value_impl] impl ChunkableModule for NextDynamicEntryModule { #[turbo_tasks::function] diff --git a/crates/next-core/src/next_server_component/server_component_module.rs b/crates/next-core/src/next_server_component/server_component_module.rs index 5a36f168ea613..7c52489f86057 100644 --- a/crates/next-core/src/next_server_component/server_component_module.rs +++ b/crates/next-core/src/next_server_component/server_component_module.rs @@ -4,9 +4,8 @@ use anyhow::Result; use indoc::formatdoc; use turbo_rcstr::rcstr; use turbo_tasks::{ResolvedVc, Vc}; -use turbo_tasks_fs::{FileContent, FileSystemPath}; +use turbo_tasks_fs::FileSystemPath; use turbopack_core::{ - asset::{Asset, AssetContent}, chunk::{ChunkItem, ChunkType, ChunkableModule, ChunkingContext, ModuleChunkItemIdExt}, ident::AssetIdent, module::{Module, ModuleSideEffects}, @@ -94,20 +93,6 @@ impl Module for NextServerComponentModule { } } -#[turbo_tasks::value_impl] -impl Asset for NextServerComponentModule { - #[turbo_tasks::function] - fn content(&self) -> Vc { - AssetContent::File( - FileContent::Content( - "// This is a marker module for Next.js server components.".into(), - ) - .resolved_cell(), - ) - .cell() - } -} - #[turbo_tasks::value_impl] impl ChunkableModule for NextServerComponentModule { #[turbo_tasks::function] diff --git a/crates/next-core/src/next_server_utility/server_utility_module.rs b/crates/next-core/src/next_server_utility/server_utility_module.rs index dd2b7c41f9592..4271301726337 100644 --- a/crates/next-core/src/next_server_utility/server_utility_module.rs +++ b/crates/next-core/src/next_server_utility/server_utility_module.rs @@ -4,9 +4,8 @@ use anyhow::Result; use indoc::formatdoc; use turbo_rcstr::rcstr; use turbo_tasks::{ResolvedVc, Vc}; -use turbo_tasks_fs::{FileContent, FileSystemPath}; +use turbo_tasks_fs::FileSystemPath; use turbopack_core::{ - asset::{Asset, AssetContent}, chunk::{ChunkItem, ChunkType, ChunkableModule, ChunkingContext, ModuleChunkItemIdExt}, ident::AssetIdent, module::{Module, ModuleSideEffects}, @@ -75,18 +74,6 @@ impl Module for NextServerUtilityModule { } } -#[turbo_tasks::value_impl] -impl Asset for NextServerUtilityModule { - #[turbo_tasks::function] - fn content(&self) -> Vc { - AssetContent::File( - FileContent::Content("// This is a marker module for Next.js server utilities.".into()) - .resolved_cell(), - ) - .cell() - } -} - #[turbo_tasks::value_impl] impl ChunkableModule for NextServerUtilityModule { #[turbo_tasks::function] diff --git a/crates/next-core/src/next_shared/transforms/swc_ecma_transform_plugins.rs b/crates/next-core/src/next_shared/transforms/swc_ecma_transform_plugins.rs index 0e2677040fdbf..93a8ea3a8061b 100644 --- a/crates/next-core/src/next_shared/transforms/swc_ecma_transform_plugins.rs +++ b/crates/next-core/src/next_shared/transforms/swc_ecma_transform_plugins.rs @@ -68,10 +68,11 @@ pub async fn get_swc_ecma_transform_rule_impl( enable_mdx_rs: bool, ) -> Result> { use anyhow::bail; - use turbo_tasks::TryFlatJoinIterExt; + use turbo_tasks::{TryFlatJoinIterExt, ValueToString}; use turbo_tasks_fs::FileContent; use turbopack_core::{ asset::Asset, + module::Module, reference_type::{CommonJsReferenceSubType, ReferenceType}, resolve::{handle_resolve_error, parse::Request, resolve}, }; @@ -133,7 +134,16 @@ pub async fn get_swc_ecma_transform_rule_impl( return Ok(None); }; - let content = &*plugin_module.content().file_content().await?; + let Some(plugin_source) = &*plugin_module.source().await? else { + use anyhow::bail; + + bail!( + "Expected source for plugin module: {}", + plugin_module.ident().to_string().await? + ); + }; + + let content = &*plugin_source.content().file_content().await?; let FileContent::Content(file) = content else { bail!("Expected file content for plugin module"); }; diff --git a/crates/next-core/src/raw_ecmascript_module.rs b/crates/next-core/src/raw_ecmascript_module.rs index 72eeaf2842571..fcbf67e724fa6 100644 --- a/crates/next-core/src/raw_ecmascript_module.rs +++ b/crates/next-core/src/raw_ecmascript_module.rs @@ -9,7 +9,7 @@ use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryJoinIterExt, ValueToStr use turbo_tasks_fs::{FileContent, rope::Rope}; use turbopack::{ModuleAssetContext, module_options::CustomModuleType}; use turbopack_core::{ - asset::{Asset, AssetContent}, + asset::Asset, chunk::{ChunkItem, ChunkType, ChunkableModule, ChunkingContext}, code_builder::CodeBuilder, compile_time_info::{ @@ -103,14 +103,6 @@ impl Module for RawEcmascriptModule { } } -#[turbo_tasks::value_impl] -impl Asset for RawEcmascriptModule { - #[turbo_tasks::function] - fn content(&self) -> Vc { - self.source.content() - } -} - #[turbo_tasks::value_impl] impl ChunkableModule for RawEcmascriptModule { #[turbo_tasks::function] diff --git a/docs/01-app/01-getting-started/05-server-and-client-components.mdx b/docs/01-app/01-getting-started/05-server-and-client-components.mdx index 53e82fe5d099d..832473b9877bf 100644 --- a/docs/01-app/01-getting-started/05-server-and-client-components.mdx +++ b/docs/01-app/01-getting-started/05-server-and-client-components.mdx @@ -412,6 +412,191 @@ Your Server Component will now be able to directly render your provider, and all > **Good to know**: You should render providers as deep as possible in the tree – notice how `ThemeProvider` only wraps `{children}` instead of the entire `` document. This makes it easier for Next.js to optimize the static parts of your Server Components. +### Sharing data with context and React.cache + +You can share fetched data across both Server and Client Components by combining [`React.cache`](https://react.dev/reference/react/cache) with context providers. + +Create a cached function that fetches data: + +```ts filename="app/lib/user.ts" switcher +import { cache } from 'react' + +export const getUser = cache(async () => { + const res = await fetch('https://api.example.com/user') + return res.json() +}) +``` + +```js filename="app/lib/user.js" switcher +import { cache } from 'react' + +export const getUser = cache(async () => { + const res = await fetch('https://api.example.com/user') + return res.json() +}) +``` + +Create a context provider that stores the promise: + +```tsx filename="app/user-provider.tsx" switcher +'use client' + +import { createContext } from 'react' + +type User = { + id: string + name: string +} + +export const UserContext = createContext | null>(null) + +export default function UserProvider({ + children, + userPromise, +}: { + children: React.ReactNode + userPromise: Promise +}) { + return {children} +} +``` + +```jsx filename="app/user-provider.js" switcher +'use client' + +import { createContext } from 'react' + +export const UserContext = createContext(null) + +export default function UserProvider({ children, userPromise }) { + return {children} +} +``` + +In a layout, pass the promise to the provider without awaiting: + +```tsx filename="app/layout.tsx" switcher +import UserProvider from './user-provider' +import { getUser } from './lib/user' + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + const userPromise = getUser() // Don't await + + return ( + + + {children} + + + ) +} +``` + +```jsx filename="app/layout.js" switcher +import UserProvider from './user-provider' +import { getUser } from './lib/user' + +export default function RootLayout({ children }) { + const userPromise = getUser() // Don't await + + return ( + + + {children} + + + ) +} +``` + +Client Components use [`use()`](https://react.dev/reference/react/use) to resolve the promise from context, wrapped in `` for fallback UI: + +```tsx filename="app/ui/profile.tsx" switcher +'use client' + +import { use, useContext } from 'react' +import { UserContext } from '../user-provider' + +export function Profile() { + const userPromise = useContext(UserContext) + if (!userPromise) { + throw new Error('useContext must be used within a UserProvider') + } + const user = use(userPromise) + return

Welcome, {user.name}

+} +``` + +```jsx filename="app/ui/profile.js" switcher +'use client' + +import { use, useContext } from 'react' +import { UserContext } from '../user-provider' + +export function Profile() { + const userPromise = useContext(UserContext) + if (!userPromise) { + throw new Error('useContext must be used within a UserProvider') + } + const user = use(userPromise) + return

Welcome, {user.name}

+} +``` + +```tsx filename="app/page.tsx" switcher +import { Suspense } from 'react' +import { Profile } from './ui/profile' + +export default function Page() { + return ( + Loading profile...}> + + + ) +} +``` + +```jsx filename="app/page.js" switcher +import { Suspense } from 'react' +import { Profile } from './ui/profile' + +export default function Page() { + return ( + Loading profile...}> + + + ) +} +``` + +Server Components can also call `getUser()` directly: + +```tsx filename="app/dashboard/page.tsx" switcher +import { getUser } from '../lib/user' + +export default async function DashboardPage() { + const user = await getUser() // Cached - same request, no duplicate fetch + return

Dashboard for {user.name}

+} +``` + +```jsx filename="app/dashboard/page.js" switcher +import { getUser } from '../lib/user' + +export default async function DashboardPage() { + const user = await getUser() // Cached - same request, no duplicate fetch + return

Dashboard for {user.name}

+} +``` + +Since `getUser` is wrapped with `React.cache`, multiple calls within the same request return the same memoized result, whether called directly in Server Components or resolved via context in Client Components. + +> **Good to know**: `React.cache` is scoped to the current request only. Each request gets its own memoization scope with no sharing between requests. + ### Third-party components When using a third-party component that relies on client-only features, you can wrap it in a Client Component to ensure it works as expected. diff --git a/docs/01-app/01-getting-started/06-cache-components.mdx b/docs/01-app/01-getting-started/06-cache-components.mdx index 83221cdb24a54..ac9d16cbb4bd5 100644 --- a/docs/01-app/01-getting-started/06-cache-components.mdx +++ b/docs/01-app/01-getting-started/06-cache-components.mdx @@ -194,6 +194,10 @@ Use [`connection()`](/docs/app/api-reference/functions/connection) if you need t > **Good to know**: Runtime data cannot be cached with `use cache` because it requires request context. Components that access runtime APIs must always be wrapped in ``. However, you can extract values from runtime data and pass them as arguments to cached functions. See the [with runtime data](#with-runtime-data) section for an example. +One approach for reading runtime data like cookies without blocking the static shell is to pass a promise to a client context provider. See [Sharing data with context and React.cache](/docs/app/getting-started/server-and-client-components#sharing-data-with-context-and-reactcache) for an example. + +> **Good to know:** `React.cache` operates in an isolated scope inside `use cache` boundaries. See [React.cache isolation](/docs/app/api-reference/directives/use-cache#reactcache-isolation) for more information. + ### Non-deterministic operations Operations like `Math.random()`, `Date.now()`, or `crypto.randomUUID()` produce different values each time they execute. To ensure these run at request time (generating unique values per request), Cache Components requires you to explicitly signal this intent by calling these operations after dynamic or runtime data access. diff --git a/docs/01-app/03-api-reference/01-directives/use-cache.mdx b/docs/01-app/03-api-reference/01-directives/use-cache.mdx index d75de012f1349..1be704ed351f4 100644 --- a/docs/01-app/03-api-reference/01-directives/use-cache.mdx +++ b/docs/01-app/03-api-reference/01-directives/use-cache.mdx @@ -189,6 +189,10 @@ async function CachedForm({ action }: { action: () => Promise }) { ## Constraints +Cached functions execute in an isolated environment. The following constraints ensure cache behavior remains predictable and secure. + +### Runtime APIs + Cached functions and components **cannot** directly access runtime APIs like `cookies()`, `headers()`, or `searchParams`. Instead, read these values outside the cached scope and pass them as arguments. ### Runtime caching considerations @@ -206,6 +210,34 @@ If the default in-memory cache isn't enough, consider **[`use cache: remote`](/d Very rarely, for compliance requirements or when you can't refactor your code to pass runtime data as arguments to a `use cache` scope, you might need [`use cache: private`](/docs/app/api-reference/directives/use-cache-private). +### React.cache isolation + +[`React.cache`](https://react.dev/reference/react/cache) operates in an isolated scope inside `use cache` boundaries. Values stored via `React.cache` outside a `use cache` function are not visible inside it. + +This means you cannot use `React.cache` to pass data into a `use cache` scope: + +```tsx +import { cache } from 'react' + +const store = cache(() => ({ current: null as string | null })) + +function Parent() { + const shared = store() + shared.current = 'value from parent' + return +} + +async function Child() { + 'use cache' + const shared = store() + // shared.current is null, not 'value from parent' + // use cache has its own isolated React.cache scope + return
{shared.current}
+} +``` + +This isolation ensures cached functions have predictable, self-contained behavior. To pass data into a `use cache` scope, use function arguments instead. + ## `use cache` at runtime On the **server**, cache entries are stored in-memory and respect the `revalidate` and `expire` times from your `cacheLife` configuration. You can customize the cache storage by configuring [`cacheHandlers`](/docs/app/api-reference/config/next-config-js/cacheHandlers) in your `next.config.js` file. diff --git a/docs/01-app/03-api-reference/04-functions/revalidatePath.mdx b/docs/01-app/03-api-reference/04-functions/revalidatePath.mdx index c25f4a238f7bc..fdaa544930346 100644 --- a/docs/01-app/03-api-reference/04-functions/revalidatePath.mdx +++ b/docs/01-app/03-api-reference/04-functions/revalidatePath.mdx @@ -22,10 +22,10 @@ description: API Reference for the revalidatePath function. revalidatePath(path: string, type?: 'page' | 'layout'): void; ``` -- `path`: Either a route pattern corresponding to the data you want to revalidate, for example `/product/[slug]`, or a specific URL, `/product/123`. Do not append `/page` or `/layout`, use the `type` parameter instead. Must not exceed 1024 characters. This value is case-sensitive. -- `type`: (optional) `'page'` or `'layout'` string to change the type of path to revalidate. If `path` contains a dynamic segment, for example `/product/[slug]`, this parameter is required. If `path` is a specific URL, `/product/1`, omit `type`. +- `path`: Either a string that represents your route file structure. This can be a literal path like `/product/123`, or a route pattern with dynamic segments like `/product/[slug]`. Do not append `/page` or `/layout`, use the `type` parameter instead. Must not exceed 1024 characters. This value is case-sensitive. You do not need to include a trailing slash, regardless of your [`trailingSlash`](/docs/app/api-reference/config/next-config-js/trailingSlash) config. +- `type`: (optional) `'page'` or `'layout'` string to change the type of path to revalidate. If `path` contains a dynamic segment, for example `/product/[slug]`, this parameter is required. If `path` is a literal path like `/product/1`, omit `type`. -Use a specific URL when you want to refresh a [single page](#revalidating-a-specific-url). Use a route pattern plus `type` to refresh [multiple URLs](#revalidating-a-page-path). +Use a literal path when you want to refresh a [single page](#revalidating-a-specific-path). Use a route pattern plus `type` to refresh [all matching pages](#revalidating-a-page-path). ## Returns @@ -49,6 +49,37 @@ export async function GET() { } ``` +## Using `revalidatePath` with rewrites + +When using [rewrites](/docs/app/api-reference/config/next-config-js/rewrites), you must pass the **destination** path (the actual route file location), not the source path that appears in the browser's address bar. + +For example, if you have a rewrite from `/blog` to `/news`: + +```js filename="next.config.js" +module.exports = { + async rewrites() { + return [ + { + source: '/blog', + destination: '/news', + }, + ] + }, +} +``` + +To revalidate this page, use the destination path: + +```ts +// Correct: use the destination path +revalidatePath('/news') + +// Incorrect: the source path won't match the cache entry +revalidatePath('/blog') +``` + +This is because `revalidatePath` operates on the route file structure, not the URL visible to users. Cache entries are tagged based on which route file renders them. + ## Relationship with `revalidateTag` and `updateTag` `revalidatePath`, [`revalidateTag`](/docs/app/api-reference/functions/revalidateTag) and [`updateTag`](/docs/app/api-reference/functions/updateTag) serve different purposes: @@ -99,14 +130,14 @@ This pattern ensures that both the specific page and any other pages using the s ## Examples -### Revalidating a specific URL +### Revalidating a specific path ```ts import { revalidatePath } from 'next/cache' revalidatePath('/blog/post-1') ``` -This will invalidate one specific URL for revalidation on the next page visit. +This will invalidate one specific path for revalidation on the next page visit. ### Revalidating a Page path @@ -117,7 +148,7 @@ revalidatePath('/blog/[slug]', 'page') revalidatePath('/(main)/blog/[slug]', 'page') ``` -This will invalidate any URL that matches the provided `page` file for revalidation on the next page visit. This will _not_ invalidate pages beneath the specific page. For example, `/blog/[slug]` won't invalidate `/blog/[slug]/[author]`. +This will invalidate any path that matches the provided `page` file for revalidation on the next page visit. This will _not_ invalidate pages beneath the specific page. For example, `/blog/[slug]` won't invalidate `/blog/[slug]/[author]`. ### Revalidating a Layout path @@ -128,7 +159,7 @@ revalidatePath('/blog/[slug]', 'layout') revalidatePath('/(main)/post/[slug]', 'layout') ``` -This will invalidate any URL that matches the provided `layout` file for revalidation on the next page visit. This will cause pages beneath with the same layout to be invalidated and revalidated on the next visit. For example, in the above case, `/blog/[slug]/[another]` would also be invalidated and revalidated on the next visit. +This will invalidate any path that matches the provided `layout` file for revalidation on the next page visit. This will cause pages beneath with the same layout to be invalidated and revalidated on the next visit. For example, in the above case, `/blog/[slug]/[another]` would also be invalidated and revalidated on the next visit. ### Revalidating all data diff --git a/packages/next/src/build/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts index f03844d47ff3d..8ad6b032e80c7 100644 --- a/packages/next/src/build/templates/app-page.ts +++ b/packages/next/src/build/templates/app-page.ts @@ -686,7 +686,7 @@ export async function handler( isDebugDynamicAccesses || isDebugFallbackShell ? { - nextExport: true, + isBuildTimePrerendering: true, supportsDynamicResponse: false, isStaticGeneration: true, isDebugDynamicAccesses: isDebugDynamicAccesses, @@ -736,12 +736,6 @@ export async function handler( }, } - if (isDebugStaticShell || isDebugDynamicAccesses) { - context.renderOpts.nextExport = true - context.renderOpts.supportsDynamicResponse = false - context.renderOpts.isDebugDynamicAccesses = isDebugDynamicAccesses - } - // When we're revalidating in the background, we should not allow dynamic // responses. if (forceStaticRender) { diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 50ab3db0c5363..4341befcdca2f 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -474,7 +474,7 @@ async function exportAppImpl( // Start the rendering process const renderOpts: WorkerRenderOptsPartial = { previewProps: prerenderManifest?.preview, - nextExport: true, + isBuildTimePrerendering: true, assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''), distDir, dev: false, diff --git a/packages/next/src/export/routes/app-route.ts b/packages/next/src/export/routes/app-route.ts index e9fbaf6573bb5..3f13e80bf7fe5 100644 --- a/packages/next/src/export/routes/app-route.ts +++ b/packages/next/src/export/routes/app-route.ts @@ -80,7 +80,7 @@ export async function exportAppRoute( renderOpts: { cacheComponents, experimental, - nextExport: true, + isBuildTimePrerendering: true, supportsDynamicResponse: false, incrementalCache, waitUntil: afterRunner.context.waitUntil, diff --git a/packages/next/src/next-devtools/README.md b/packages/next/src/next-devtools/README.md index b04c77f521c6b..ec6bed8dde5f2 100644 --- a/packages/next/src/next-devtools/README.md +++ b/packages/next/src/next-devtools/README.md @@ -31,7 +31,7 @@ This will start the Storybook server at `http://localhost:6006`. ### Styling -Next.js direcly injects CSS into the DOM via `