How to Fix: Router asPath doesn’t match between SSR and CSR

6 min read

Next.js “Router asPath doesn’t match between SSR and CSR” Fix

This error happens because the server-rendered URL and the client-side router state do not agree during hydration. In the reproduction case, visiting the demo repository at /auto-export?query=true causes Next.js to render one value on the server and a different router.asPath value on the client, which triggers a mismatch warning or hydration error.

Understanding the Root Cause

The key issue is that auto-exported pages and statically optimized pages do not always know the final browser URL, including the full query string, at build time or during the initial server render. When your component reads router.asPath too early, it may produce markup based on a value that differs between:

  • SSR/initial HTML: what Next.js can safely render before the browser router is ready
  • CSR/hydration: what the browser router computes after loading the page

On routes like /auto-export?query=true, the server can output markup without the same router state the browser later provides. If your UI directly renders router.asPath, conditional branches, keys, or derived values from it during the first render, React sees different HTML between server and client and reports a hydration mismatch.

This is especially common when using:

  • Automatically statically optimized pages
  • Query-string-dependent rendering
  • router.asPath before router.isReady becomes true
  • Conditional rendering that changes structure based on the URL

In short: router.asPath is not guaranteed to be stable during the first render across SSR and CSR. The fix is to defer reading it until the router is ready on the client, or to use a server-safe alternative when rendering initial HTML.

Step-by-Step Solution

The safest pattern is to avoid using router.asPath for initial render output. Instead, wait until router.isReady is true, then read the path on the client.

1. Identify code that renders router.asPath immediately

A problematic component usually looks like this:

import { useRouter } from 'next/router'
export default function Page() {
  const router = useRouter()

  return <div>Current path: {router.asPath}</div>
}

This can break on statically optimized or auto-exported pages because the server and client may not agree on the value during hydration.

2. Gate access with router.isReady

Update the component so the URL-dependent UI only renders after the router is ready:

import { useRouter } from 'next/router'
export default function Page() {
  const router = useRouter()

  if (!router.isReady) {
    return <div>Loading...</div>
  }

  return <div>Current path: {router.asPath}</div>
}

This prevents React from hydrating markup generated from an unstable router value.

3. If needed, move asPath into client state

If the page must avoid any mismatch at all, store the value after mount:

import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
export default function Page() {
  const router = useRouter()
  const [asPath, setAsPath] = useState('')

  useEffect(() => {
    if (router.isReady) {
      setAsPath(router.asPath)
    }
  }, [router.isReady, router.asPath])

  return <div>Current path: {asPath || 'Loading...'}</div>
}

This pattern ensures the displayed value is purely client-derived, which eliminates SSR/CSR disagreement.

4. Prefer router.query or server-provided props when possible

If you only need query parameters, do not always rely on asPath. For many cases, router.query, getServerSideProps, or data passed from the server is more predictable.

import { useRouter } from 'next/router'
export default function Page() {
  const router = useRouter()

  if (!router.isReady) {
    return <div>Loading...</div>
  }

  return <div>Query: {String(router.query.query)}</div>
}

If the page must be request-aware on first render, use getServerSideProps instead of relying on a statically optimized page:

export async function getServerSideProps(context) {
  return {
    props: {
      initialQuery: context.query.query || null,
      initialResolvedUrl: context.resolvedUrl || null,
    },
  }
}
export default function Page({ initialQuery, initialResolvedUrl }) {
  return (
    <div>
      <div>Query: {String(initialQuery)}</div>
      <div>Resolved URL: {initialResolvedUrl}</div>
    </div>
  )
}

This approach is best when the initial HTML must exactly match the incoming request URL.

5. Avoid URL-based structural differences during hydration

Even with guards, be careful not to use router.asPath for:

  • React key values on top-level elements
  • Choosing entirely different component trees on first render
  • Generating IDs that must match server markup

For example, this is risky:

const router = useRouter()
return router.asPath.includes('query=true') ? <A /> : <B />

Safer version:

const router = useRouter()
if (!router.isReady) return <div>Loading...</div>
return router.asPath.includes('query=true') ? <A /> : <B />

For the reported bug, the most practical fix is:

  1. Find any render-time use of router.asPath on the affected page.
  2. Wrap that logic behind router.isReady.
  3. If the value is only needed in the browser, populate it inside useEffect.
  4. If the page needs request-accurate HTML on first response, switch to getServerSideProps.

Common Edge Cases

Shallow routing

With shallow routing, the URL may change without a full data refetch. If your component derives important state from asPath, ensure it updates correctly when the route changes client-side.

Rewrites and redirects

Rewrites can make the browser-visible URL differ from the internal route. In those cases, pathname, route, and asPath may each tell a slightly different story. Use the one that matches your rendering goal.

Dynamic routes

On dynamic routes, params may not be available in the same shape during the earliest render. Guard code that depends on slug values, especially on statically generated pages with fallback behavior.

Base path and locale prefixes

If your app uses basePath or i18n routing, the string in asPath may include prefixes that your server-side assumptions do not account for. Avoid string parsing when structured route data is available.

Hydration-safe placeholders

If you render a loading placeholder while waiting for router.isReady, make sure that placeholder itself is stable and does not depend on browser-only APIs like window.location during the first render.

FAQ

Why does this only happen on some pages?

It usually appears on statically optimized or auto-exported pages where the server does not have the exact same routing context the browser later provides. Pages using getServerSideProps often avoid this because request data is available during rendering.

Is router.pathname safer than router.asPath?

Often, yes. router.pathname is more about the route pattern, while router.asPath includes the browser-visible path and query string. If you do not need the exact URL, pathname or router.query is typically less fragile.

Should I always wait for router.isReady?

Wait for router.isReady when your rendered output depends on client router fields that may not be stable during hydration, especially asPath and query-derived UI on static pages. If the value must be present in initial HTML, fetch it server-side instead.

By treating router.asPath as a client-ready value rather than a universally safe SSR value, you can eliminate this mismatch and keep Next.js hydration predictable.

Leave a Reply

Your email address will not be published. Required fields are marked *