diff --git a/errors/next-prerender-data.md b/errors/next-prerender-data.md deleted file mode 100644 index cad8b5323cc327..00000000000000 --- a/errors/next-prerender-data.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: Cannot access data without either defining a fallback UI to use while the data loads or caching the data ---- - -#### Why This Error Occurred - -When the experimental flag `dynamicIO` is enabled, Next.js expects you to explicitly describe whether data accessed during render should be evaluated ahead of time, while prerendering, or at Request time while rendering. - -Data in this context refers to both reading from the request using Next.js built-in Request functions like `cookies()`, `headers()`, `draftMode()`, and `connection()` functions, Next.js built-in request props `params`, and `searchParams`, as well as any asynchronous data fetching technique such as `fetch()` or other network request library or database clients and more. - -By default, any data accessed during render is treated as if it should be evaluated at request time. To explicitly communicate to Next.js that some data should be prerenderable, you must explicitly cache it using `"use cache"` or `unstable_cache`. - -However, even if you have carefully ensured that a route is fully or partially prerenderable, it's possible to inadvertently make it non-prerenderable by introducing a new data dependency that hasn't been cached. To prevent this, Next.js requires that data accessed without caching must be inside a Suspense boundary that defines a fallback UI to use while loading this data. - -This makes React's `Suspense` component an explicit opt-in to allow uncached data access. - -To ensure you have a fully prerenderable route, you should omit any Suspense boundaries in your route. Suspense is useful for loading UI dynamically but if you have entirely prerenderable pages there is no need to have fallback UI because the primary UI will always be available. - -To allow uncached data anywhere in your application, you can add a Suspense boundary inside your `` tag in your Root Layout. However, we don't recommend you do this because you will likely want to scope Suspense boundaries around more granular component boundaries that provide fallback UI specific to individual Components. - -Hybrid applications will typically use a combination of both techniques, with your top level shared Layouts being prerendered for static pages (without Suspense), and your layouts that actually have data dependencies defining fallback UI. - -> **Note**: While external data can be accessed inside `"use cache"` and `unstable_cache()`, Request data such as `cookies()` cannot because we don't know about cookies before a Request actually occurs. If your application needs to read cookies the only recourse you have is to opt into allowing this data read using `Suspense`. - -#### Possible Ways to Fix It - -If you are accessing external data that doesn't change often and your use case can tolerate stale results while revalidating the data after it gets too old, you should wrap the data fetch in a `"use cache"` function. This will instruct Next.js to cache this data and allow it to be accessed without defining a fallback UI for the component that is accessing this data. - -Before: - -```jsx filename="app/page.js" -async function getRecentArticles() { - return db.query(...) -} - -export default async function Page() { - const articles = await getRecentArticles(token); - return -} -``` - -After: - -```jsx filename="app/page.js" -async function getRecentArticles() { - "use cache" - // This cache can be revalidated by webhook or server action - // when you call revalidateTag("articles") - cacheTag("articles") - // This cache will revalidate after an hour even if no explicit - // revalidate instruction was received - cacheLife('hours') - return db.query(...) -} - -export default async function Page() { - const articles = await getRecentArticles(token); - return -} -``` - -If you are accessing external data that should be up to date on every single request, you should find an appropriate component to wrap in Suspense and provide a fallback UI to use while this data loads. - -Before: - -```jsx filename="app/page.js" -async function getLatestTransactions() { - return db.query(...) -} - -export default async function Page() { - const transactions = await getLatestTransactions(token); - return -} -``` - -After: - -```jsx filename="app/page.js" -import { Suspense } from 'react' - -async function getLatestTransactions() { - return db.query(...) -} - -function TransactionSkeleton() { - return
...
-} - -export default async function Page() { - const transactions = await getLatestTransactions(token); - return ( - }> - - - ) -} -``` - -If you are accessing request data like cookies you might be able to move the cookies call deeper into your component tree in a way that it already is accessed inside a Suspense boundary. - -Before: - -```jsx filename="app/inbox.js" -export async function Inbox({ token }) { - const email = await getEmail(token) - return ( -
    - {email.map((e) => ( - - ))} -
- ) -} -``` - -```jsx filename="app/page.js" -import { cookies } from 'next/headers' - -import { Inbox } from './inbox' - -export default async function Page() { - const token = (await cookies()).get('token') - return ( - - - - ) -} -``` - -After: - -```jsx filename="app/inbox.js" -import { cookies } from 'next/headers' - -export async function Inbox() { - const token = (await cookies()).get('token') - const email = await getEmail(token) - return ( -
    - {email.map((e) => ( - - ))} -
- ) -} -``` - -```jsx filename="app/page.js" -import { Inbox } from './inbox' - -export default async function Page() { - return ( - - - - ) -} -``` - -If your request data cannot be moved, you must provide a Suspense boundary somewhere above this component. - -### Useful Links - -- [`Suspense` React API](https://react.dev/reference/react/Suspense) -- [`headers` function](https://nextjs.org/docs/app/api-reference/functions/headers) -- [`cookies` function](https://nextjs.org/docs/app/api-reference/functions/cookies) -- [`draftMode` function](https://nextjs.org/docs/app/api-reference/functions/draft-mode) -- [`connection` function](https://nextjs.org/docs/app/api-reference/functions/connection) diff --git a/errors/next-prerender-missing-suspense.md b/errors/next-prerender-missing-suspense.md new file mode 100644 index 00000000000000..771995e4bb7048 --- /dev/null +++ b/errors/next-prerender-missing-suspense.md @@ -0,0 +1,262 @@ +--- +title: Cannot access data, headers, params, searchParams, or a short-lived cache a Suspense boundary nor a `"use cache"` above it. +--- + +#### Why This Error Occurred + +When the experimental flag `dynamicIO` is enabled, Next.js expects a parent `Suspense` boundary around any component that awaits data that should must accessed on every user request. The purpose of this requirement is so that Next.js can provide a useful fallback while this data is accessed and rendered. + +While some data is inherently only available when a user request is being handled, such as request headers, Next.js assumes that by default any asynchronous data is expected to be accessed each time a user request is being handled unless you specifically cache it using `"use cache"`. + +The proper fix for this specific error depends on what data you are accessing and how you want your Next.js app to behave. + +#### Possible Ways to Fix It + +##### Accessing Data + +When you access data using `fetch`, a database client, or any other module which does asynchronous IO, Next.js interprets your intent as expecting the data to load on every user request. + +If you are expecting this data to be used while fully or partially prerendering a page you must cache is using `"use cache"`. + +Before: + +```jsx filename="app/page.js" +async function getRecentArticles() { + return db.query(...) +} + +export default async function Page() { + const articles = await getRecentArticles(token); + return +} +``` + +After: + +```jsx filename="app/page.js" +async function getRecentArticles() { + "use cache" + // This cache can be revalidated by webhook or server action + // when you call revalidateTag("articles") + cacheTag("articles") + // This cache will revalidate after an hour even if no explicit + // revalidate instruction was received + cacheLife('hours') + return db.query(...) +} + +export default async function Page() { + const articles = await getRecentArticles(token); + return +} +``` + +If this data should be accessed on every user request you must provide a fallback UI using `Suspense` from React. Where you put this Suspense boundary in your application should be informed by the kind of fallback UI you want to render. It can be just above the component accessing this data or even in your Root Layout. + +Before: + +```jsx filename="app/page.js" +async function getLatestTransactions() { + return db.query(...) +} + +export default async function Page() { + const transactions = await getLatestTransactions(token); + return +} +``` + +After: + +```jsx filename="app/page.js" +import { Suspense } from 'react' + +async function getLatestTransactions() { + return db.query(...) +} + +function TransactionSkeleton() { + return
...
+} + +export default async function Page() { + const transactions = await getLatestTransactions(token); + return ( + }> + + + ) +} +``` + +##### Headers + +If you are accessing request headers using `headers()`, `cookies()`, or `draftMode()`. Consider whether you can move the use of these APIs deeper into your existing component tree. + +Before: + +```jsx filename="app/inbox.js" +export async function Inbox({ token }) { + const email = await getEmail(token) + return ( +
    + {email.map((e) => ( + + ))} +
+ ) +} +``` + +```jsx filename="app/page.js" +import { cookies } from 'next/headers' + +import { Inbox } from './inbox' + +export default async function Page() { + const token = (await cookies()).get('token') + return ( + + + + ) +} +``` + +After: + +```jsx filename="app/inbox.js" +import { cookies } from 'next/headers' + +export async function Inbox() { + const token = (await cookies()).get('token') + const email = await getEmail(token) + return ( +
    + {email.map((e) => ( + + ))} +
+ ) +} +``` + +```jsx filename="app/page.js" +import { Inbox } from './inbox' + +export default async function Page() { + return ( + + + + ) +} +``` + +Alternatively you can add a Suspense boundary above the component that is accessing Request headers. + +##### Params and SearchParams + +Layout `params` and Page `params` and `searchParams` props are promises. If you await them in the Layout or Page component you might be accessing these props higher than is actually required. Try passing these props to deeper components as a promise and awaiting them closer to where the actual param or searchParam is required + +Before: + +```jsx filename="app/map.js" +export async function Map({ lat, lng }) { + const mapData = await fetch(`https://...?lat=${lat}&lng=${lng}`) + return drawMap(mapData) +} +``` + +```jsx filename="app/page.js" +import { cookies } from 'next/headers' + +import { Map } from './map' + +export default async function Page({ searchParams }) { + const { lat, lng } = await searchParams; + return ( + + + + ) +} +``` + +After: + +```jsx filename="app/map.js" +export async function Map({ coords }) { + const { lat, lng } = await coords + const mapData = await fetch(`https://...?lat=${lat}&lng=${lng}`) + return drawMap(mapData) +} +``` + +```jsx filename="app/page.js" +import { cookies } from 'next/headers' + +import { Map } from './map' + +export default async function Page({ searchParams }) { + const coords = searchParams.then(sp => { lat: sp.lat, lng: sp.lng }) + return ( + + + + ) +} +``` + +Alternatively you can add a Suspense boundary above the component that is accessing `params` or `searchParams` so Next.js understands what UI should be used when while waiting for this request data to be accessed. + +##### Short-lived Caches + +`"use cache"` allows you to describe a `cacheLife()` that might be too short to be practical to prerender. The utility of doing this is that it can still describe a non-zero caching time for the client router cache to reuse the cache entry in the browser and it can also be useful for protecting upstream APIs while experiencing high request traffic. + +If you expected the `"use cache"` entry to be prerenderable try describing a slightly longer `cacheLife()`. + +Before: + +```jsx filename="app/page.js" +async function getDashboard() { + "use cache" + // This cache will revalidate after 1 second. It is so short + // Next.js won't prerender it on the server but the client router + // can reuse the result for up to 30 seconds unless the user manually refreshes + cacheLife('seconds') + return db.query(...) +} + +export default async function Page() { + const data = await getDashboard(token); + return +} +``` + +After: + +```jsx filename="app/page.js" +async function getDashboard() { + "use cache" + // This cache will revalidate after 1 minute. It's long enough that + // Next.js will still produce a fully or partially prerendered page + cacheLife('minutes') + return db.query(...) +} + +export default async function Page() { + const data = await getDashboard(token); + return +} +``` + +Alternatively you can add a Suspense boundary above the component that is accessing this short-lived cached data so Next.js understands what UI should be used while accessing this data on a user request. + +### Useful Links + +- [`Suspense` React API](https://react.dev/reference/react/Suspense) +- [`headers` function](https://nextjs.org/docs/app/api-reference/functions/headers) +- [`cookies` function](https://nextjs.org/docs/app/api-reference/functions/cookies) +- [`draftMode` function](https://nextjs.org/docs/app/api-reference/functions/draft-mode) +- [`connection` function](https://nextjs.org/docs/app/api-reference/functions/connection) diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index 4024dc9e29954c..4b68b210d3a254 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -605,7 +605,7 @@ export function trackAllowedDynamicAccess( dynamicValidation.hasSyncDynamicErrors = true return } else { - const message = `In Route "${route}" this component accessed data without a Suspense boundary above it to provide a fallback UI. See more info: https://nextjs.org/docs/messages/next-prerender-data` + const message = `Route "${route}": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. We don't have the exact line number added to error messages yet but you can see which component in the stack below. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense` const error = createErrorWithComponentStack(message, componentStack) dynamicValidation.dynamicErrors.push(error) return diff --git a/test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.prospective-fallback.test.ts b/test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.prospective-fallback.test.ts index 48ca0b9ef09e3d..ab021df70a7fbe 100644 --- a/test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.prospective-fallback.test.ts +++ b/test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.prospective-fallback.test.ts @@ -28,7 +28,7 @@ describe(`Dynamic IO Prospective Fallback`, () => { } expect(next.cliOutput).toContain( - 'In Route "/blog/[slug]" this component accessed data without a Suspense boundary above it to provide a fallback UI.' + 'Route "/blog/[slug]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it.' ) }) @@ -46,7 +46,7 @@ describe(`Dynamic IO Prospective Fallback`, () => { await next.start() expect(next.cliOutput).not.toContain( - 'In Route "/blog/[slug]" this component accessed data without a Suspense boundary above it to provide a fallback UI.' + 'Route "/blog/[slug]": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it.' ) }) } diff --git a/test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.test.ts b/test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.test.ts index 99da76207ed852..970be876b88670 100644 --- a/test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.test.ts +++ b/test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.test.ts @@ -322,7 +322,7 @@ function runTests(options: { withMinification: boolean }) { const expectError = createExpectError(next.cliOutput) expectError( - 'In Route "/" this component accessed data without a Suspense boundary above it to provide a fallback UI.', + 'Route "/": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it.', // Turbopack doesn't support disabling minification yet withMinification || isTurbopack ? undefined : 'IndirectionTwo' ) @@ -331,7 +331,7 @@ function runTests(options: { withMinification: boolean }) { // one task actually reports and error at the moment. We should fix upstream but for now we exclude the second error when PPR is off // because we are using canary React and renderToReadableStream rather than experimental React and prerender expectError( - 'In Route "/" this component accessed data without a Suspense boundary above it to provide a fallback UI.', + 'Route "/": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it.', // Turbopack doesn't support disabling minification yet withMinification || isTurbopack ? undefined : 'IndirectionThree' )