Back to Next Js

Next.js encountered the unstable value Date.now() in a Client Component

errors/blocking-prerender-current-time-client.mdx

16.3.015.7 KB
Original Source
<div style={{ padding: '1.25rem 1.5rem', border: '1px solid var(--ds-gray-400)', borderRadius: '12px', background: 'var(--ds-background-200)', margin: '1.5rem 0 2rem', fontSize: '0.95rem', lineHeight: '1.6', }} > This Insight is part of the [Instant Navigations](https://nextjs.org/blog/next-16-3-instant-navigations) feature introduced in Next.js 16.3. If you're new to it, start with the [Ensuring instant navigations](https://preview.nextjs.org/docs/app/guides/instant-navigation) guide for an overview of what instant navigations are and how Next.js validates them, then come back here for the specific fix. </div>

A Client Component called Date.now(), Date(), or new Date() inline during render, and the surrounding tree had no <Suspense> boundary. Client Components are server-side rendered on first load, so Next.js can't bake "now" into the prerendered HTML. The SSR timestamp won't match the value the client computes on hydration, so you need to choose: defer the value behind a <Suspense> boundary so SSR can stream it, or move the call into useEffect (or an event handler) so it only runs on the client.

The Server Component case is handled at Date.now() during prerendering. Other unpredictable client-side APIs (Math.random(), crypto.randomUUID()) have parallel error pages: Math.random() in a Client Component and Crypto APIs in a Client Component.

Ways to fix this

<FixCardGrid> <FixCard group="stream" title="Wrap in or move into Suspense" href="#wrap-in-or-move-into-suspense" snippets={[ { text: '<Suspense fallback={…}>', highlight: true }, { text: ' <DateDisplay />' }, { text: '</Suspense>', highlight: true }, ]} /> <FixCard group="defer" title="Move into effect or event handler" href="#move-into-effect-or-event-handler" snippets={[ { text: '<button onClick={() => {', highlight: true }, { text: ' setT(Date.now())' }, { text: '}} />' }, ]} /> <FixCard group="measure" title="For telemetry, use a timing API" href="#for-telemetry-use-a-timing-api" snippets={[ { text: 'const start = performance.now()', highlight: true }, { text: 'doWork()' }, { text: 'const ms = performance.now() - start' }, ]} /> </FixCardGrid>

Wrap in or move into Suspense

Choose this fix when the timestamp is part of the rendered output and a brief fallback during SSR is acceptable. Wrap the consuming Client Component in <Suspense> from its parent. The fallback ships in the prerendered HTML, and Next.js fills in the real component when the browser hydrates.

Patterns

Wrap from a Server Component parent

Place the <Suspense> boundary in the Server Component that renders the Client Component. The fallback prerenders. The inner Client Component runs in the browser.

jsx
import { Suspense } from 'react'
import { RelativeTime } from './relative-time'

export default function Article({ timestamp }) {
  return (
    <article>
      <Suspense fallback={<time></time>}>
        <RelativeTime timestamp={timestamp} />
      </Suspense>
    </article>
  )
}
jsx
'use client'

export function RelativeTime({ timestamp }) {
  const now = Date.now()
  return (
    <time suppressHydrationWarning>{computeTimeAgo({ timestamp, now })}</time>
  )
}

Learn more: Streaming.

Trade-off

The component shows the fallback during SSR and the first paint. For above-the-fold UI this can be visible. Pick a fallback that matches the final layout so it doesn't cause a layout shift when the component hydrates. See minimizing layout shift.

Gotchas

  • Any UI rendered as part of the prerender shell must be deterministic, including <Suspense> fallbacks, loading.js, error.js, not-found.js, and global-error.js. Calling Date.now() in any of them raises this same error. Use stable placeholder content.
  • The inner Client Component still runs during SSR, behind the boundary. If you need to guarantee the timestamp only runs in the browser, use Move into effect or event handler instead.
  • A <Suspense> boundary only fixes the prerender/hydration mismatch, not client re-renders. If the component using Date.now() re-renders on the client (a parent state change, a context update), it reads a fresh timestamp each time. To stabilize the value across re-renders, capture Date.now() once in a useState initializer or useRef, or compute it on the server and pass it down as a prop.

Move into effect or event handler

Choose this fix when the timestamp isn't needed for the first paint. Move the Date.now() call into useEffect (for first-paint-after-mount values) or an event handler (for interaction values). The initial render uses a deterministic placeholder, so SSR and hydration agree.

Patterns

Use useEffect to initialize after mount

For displays that should update over time (a relative-time label, a stopwatch). Initialize state to a deterministic placeholder, then assign the real value in useEffect.

jsx
'use client'

import { startTransition, useEffect, useState } from 'react'

export function Clock() {
  const [now, setNow] = useState(null)
  useEffect(() => {
    // Wrap in startTransition so that if any component below suspends
    // during this update, React keeps the existing UI visible instead
    // of flashing the nearest outer <Suspense> fallback.
    startTransition(() => {
      setNow(Date.now())
    })
    const id = setInterval(() => {
      startTransition(() => {
        setNow(Date.now())
      })
    }, 1000)
    return () => clearInterval(id)
  }, [])
  return <time>{now ? new Date(now).toLocaleTimeString() : '…'}</time>
}

Learn more: useEffect.

Compute on user interaction

When the timestamp is in response to a click ("mark as read", "snapshot now"), compute it in the event handler.

jsx
'use client'

import { useState } from 'react'

export function Snapshot() {
  const [taken, setTaken] = useState(null)
  return (
    <button onClick={() => setTaken(Date.now())}>
      {taken ? `Snapshot at ${new Date(taken).toLocaleString()}` : 'Snapshot'}
    </button>
  )
}

Trade-off

The user sees the placeholder briefly before the real timestamp. For interactions the wait is invisible, but for useEffect-based values there's a flash of the initial state. See Preventing flash before hydration for techniques that eliminate the flash.

Gotchas

  • Don't compute the timestamp inline during render with a lazy initializer like useState(() => Date.now()). The initializer still runs during SSR and triggers the error.
  • If the value needs to be hydration-stable, use Wrap in or move into Suspense instead.
  • When you call setState from inside useEffect, wrap it in startTransition. Cascading state updates during hydration can cause an outer <Suspense> boundary's fallback to briefly flash. startTransition marks the update as non-blocking so React keeps the existing UI in place while the new value resolves.

For telemetry, use a timing API

Choose this fix when the timestamp isn't user-visible at all. Logging, performance instrumentation, span correlation: all measurements that need a clock but don't render anything. Switch to performance.now(), a high-resolution monotonic timer that doesn't carry the same semantic ("the current wall-clock time") that prevents prerendering.

Patterns

Replace Date.now() with performance.now()

Drop-in replacement for any elapsed-time calculation.

jsx
'use client'

import { useEffect } from 'react'

export function Timed() {
  useEffect(() => {
    const start = performance.now()
    doWork()
    const elapsedMs = performance.now() - start
    console.log(`doWork took ${elapsedMs}ms`)
  }, [])
  return null
}

Learn more: performance.now().

Trade-off

performance.now() returns a high-resolution timestamp relative to time origin, not a wall-clock time. Use it only for durations.

Gotchas

  • performance.now() values from the server and browser can't be compared. Each environment has its own time origin.
  • Don't pass a performance.now() value into the rendered output. It's non-deterministic between SSR and the browser.
  • For absolute time in an observability tool, use performance.timeOrigin + performance.now() to get a wall-clock timestamp without tripping this error.

Other options

Cache the value in a Server Component

When the timestamp doesn't need to reflect the user's current visit and lives inside a Client Component only because of where it's rendered, lift the read into a Server Component above with use cache. The canonical case is a copyright year in a footer.

jsx
import { cacheLife } from 'next/cache'

async function getCurrentYear() {
  'use cache'
  cacheLife('max')
  return new Date().getFullYear()
}

export default async function Layout({ children }) {
  return (
    <>
      <main>{children}</main>
      <footer>Copyright {await getCurrentYear()}</footer>
    </>
  )
}

Learn more: Date.now() during prerendering.

Suspend with use(io())

When the read genuinely needs to happen per visit and you can't move it to an effect or event, call io() from next/cache before the read with React's use hook. Client Components prerender on the server during SSR, where the read would otherwise be included in the static shell. use(io()) suspends the prerender so the component is excluded from the shell and rendered on every request from the nearest <Suspense> boundary.

jsx
'use client'
import { use } from 'react'
import { io } from 'next/cache'

export function LastUpdated() {
  use(io())
  return <span>Updated at {new Date().toLocaleTimeString()}</span>
}

Wrap the component in <Suspense> so the surrounding shell stays prerendered.

jsx
import { Suspense } from 'react'
import { LastUpdated } from './components/last-updated'

export default function Page() {
  return (
    <Suspense fallback={null}>
      <LastUpdated />
    </Suspense>
  )
}

Learn more: io.

Verifying the fix

After applying a fix, reload the route and confirm the page immediately paints meaningful UI, with any <Suspense> fallbacks covering only the regions that stream in. A <Suspense> boundary placed around the whole page body can pass validation with an empty shell, which defeats the point of an instant navigation.

In next dev, the error overlay points at the failing component with file paths and line numbers. When working from a build instead, the default next build output is more abbreviated. Run next build --debug-prerender for full user-frame stack traces and next build --debug-build-paths /dashboard /settings to iterate on specific routes.

Why instant = false doesn't clear this error

This error fires from the prerender, not from instant-navigation validation. new Date() and Date.now() return a different value on every render, so the prerender can't bake them into a static shell regardless of the segment's instant config or experimental.instantInsights.validationLevel. Use one of the fixes above.