Skip to content

Commit

Permalink
[dynamicIO] update data access error and documentation
Browse files Browse the repository at this point in the history
It will be quite common to attempt to access data during prerendering and encounter the error that indicates you did not provide a Suspense boundary to define fallback UI. This change updates the error message and includes a link to a new docs page which explains how to handle this error.
  • Loading branch information
gnoff committed Oct 23, 2024
1 parent d61d631 commit 69f187d
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 3 deletions.
170 changes: 170 additions & 0 deletions errors/next-prerender-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
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, you may have taken great care to have a fully or partially prerenderable route and it would be quite easy to accidentally make such a route non-prerenderable by introducing a new data dependency you forgot to cache. 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 allowing uncached data access.

To ensure you have a fully prerenerable 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 just inside your `<body>` tag in your Root Layout. We don't recommend you do this however 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 avoiding Suspense so they can be prerendered for static pages and your layouts that actually have data dependencies will define 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 <ArticleList articles={articles}>
}
```

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 <ArticleList articles={articles}>
}
```

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 <TransactionList transactions={transactions}>
}
```

After:

```jsx filename="app/page.js"
import { Suspense } from 'react'

async function getLatestTransactions() {
return db.query(...)
}

function TransactionSkeleton() {
return <div>...</div>
}

export default async function Page() {
const transactions = await getLatestTransactions(token);
return (
<Suspense fallback={<TransactionSkeleton />}>
<TransactionList transactions={transactions}>
</Suspense>
)
}
```

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 (
<ul>
{email.map((e) => (
<EmailRow key={e.id} />
))}
</ul>
)
}
```

```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 (
<Suspense fallback="loading your inbox...">
<Inbox token={token}>
</Suspense>
)
}
```

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 (
<ul>
{email.map((e) => (
<EmailRow key={e.id} />
))}
</ul>
)
}
```

```jsx filename="app/page.js"
import { Inbox } from './inbox'

export default async function Page() {
return (
<Suspense fallback="loading your inbox...">
<Inbox>
</Suspense>
)
}
```

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)
4 changes: 1 addition & 3 deletions packages/next/src/server/app-render/dynamic-rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,9 +605,7 @@ export function trackAllowedDynamicAccess(
dynamicValidation.hasSyncDynamicErrors = true
return
} else {
// The thrownValue must have been the RENDER_COMPLETE abortReason because the only kinds of errors tracked here are
// interrupts or render completes
const message = `In Route "${route}" this component accessed data without a fallback UI available somewhere above it using Suspense.`
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 error = createErrorWithComponentStack(message, componentStack)
dynamicValidation.dynamicErrors.push(error)
return
Expand Down

0 comments on commit 69f187d

Please sign in to comment.