unstable_noStore()

Opt a component out of static rendering so its data is fetched on every request.

Signature
unstable_noStore(): void
  • Added in Next.js v14.0.0
  • Deprecated since PHP v15.0.0

Return value

voidNothing. The enclosing scope is marked as uncacheable and opted out of prerendering.

Changelog

  • PHP v14.0.0unstable_noStore introduced.
  • PHP v15.0.0Deprecated in favor of connection().

unstable_noStore declaratively opts the enclosing scope out of prerendering and marks it as not cacheable. It was the answer for data sources that do not go through fetch — an ORM query, an SDK call — where you could not simply pass cache: 'no-store'.

Status

This is a legacy API, still supported for backward compatibility. Since version 15 the recommended replacement is connection() from next/server, which expresses the same intent — wait for an actual request before rendering this — without the caching-era naming.

What trips people up

  • It is per-component, which is the reason to prefer it over export const dynamic = 'force-dynamic': the rest of the route can still be prerendered. People who reach for the segment config instead lose static rendering for the whole page.
  • Inside unstable_cache it does not opt out of static generation — the cache configuration wins. Calling it there to "force freshness" silently does nothing.
  • It is equivalent to cache: 'no-store' on a fetch, so mixing both is redundant rather than doubly effective.

Example

Example · typescript
import { unstable_noStore as noStore } from 'next/cache'
import { db } from '@/lib/db'

export default async function LiveTotal() {
  noStore()

  const result = await db.orders.aggregate({ _sum: { amount: true } })

  return <p>Total: {result._sum.amount}</p>
}

When to use it

  • Keep one live figure uncached on an otherwise static page, in a Next.js 14 codebase.
  • Opt a database-backed component out of prerendering where no fetch call exists to configure.
  • Replace a route-wide dynamic = 'force-dynamic' with a narrower, per-component opt-out.

Frequently asked questions

What replaces unstable_noStore in Next.js 15?

connection() from next/server. It is the recommended replacement and is awaited rather than called synchronously; unstable_noStore remains supported for backward compatibility but is a legacy API.

Is unstable_noStore better than export const dynamic = 'force-dynamic'?

Yes for narrow opt-outs, because it is granular and can be used on a per-component basis while the segment config applies to the whole route. The rest of the page can still be prerendered around the one component that needs request-time data.

Why isn't unstable_noStore making my data fresh?

If the call sits inside unstable_cache, it does not opt out of static generation — the cache configuration determines whether the result is cached. Move the query out of the cached wrapper, or drop the wrapper entirely.

More examples

The recommended replacement

The same component written for Next.js 15 and later. connection() is awaited rather than called synchronously, and it states the actual requirement — this render needs a real incoming request — instead of describing a cache mode.

Example · typescript
import { connection } from 'next/server'
import { db } from '@/lib/db'

export default async function LiveTotal() {
  // Defer to request time
  await connection()

  const result = await db.orders.aggregate({ _sum: { amount: true } })

  return <p>Total: {result._sum.amount}</p>
}

Narrow opt-out inside a static page

Lives at app/dashboard/page.tsx. Only LiveTotal is excluded from the prerender; the shell around it is still static. This is the difference from export const dynamic = 'force-dynamic', which would make the entire route dynamic.

Example · typescript
import { Suspense } from 'react'
import LiveTotal from './live-total'

export default function DashboardPage() {
  return (
    <main>
      <h1>Dashboard</h1>
      {/* This heading and layout stay prerendered */}
      <Suspense fallback={<p>Loading total...</p>}>
        <LiveTotal />
      </Suspense>
    </main>
  )
}

Where it has no effect

Shows the trap. Inside unstable_cache the call does not opt out of static generation — the result is still cached for 60 seconds. If you need fresh data, do not wrap the function in the first place.

Example · typescript
import { unstable_cache, unstable_noStore as noStore } from 'next/cache'
import { db } from '@/lib/db'

export const getStats = unstable_cache(
  async () => {
    noStore() // ignored here: the cache configuration decides
    return db.stats.findFirst()
  },
  ['stats'],
  { revalidate: 60 }
)

Discussion

  • Be the first to comment on this lesson.