Next.js

Fix: useRouter query undefined on first render

Why Next.js hydrates a dynamic Pages Router page with an empty query object, how router.isReady fixes it, and why the App Router does not have this exact problem.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

A dynamic Pages Router page like pages/[lang]/abc.js renders two or three times before router.query.lang stops being undefined. This is not a race condition in your code: it is how Next.js's Pages Router hydrates a statically optimized page, and it is specific to next/router — the App Router's useRouter does not have this property at all.

TL;DR#

A Pages Router page with no getServerSideProps, getStaticProps or getInitialProps is pre-rendered into one static HTML file that does not know the dynamic route segment. That file hydrates with router.query equal to {} and router.isReady equal to false. Guard every read of router.query with router.isReady inside a useEffect, or force the page to render server-side with getServerSideProps so the segment is known on the very first response.

jsx
// pages/[lang]/abc.js — first render, no data-fetching function
router.isReady // false
router.query   // {}

Why it happens#

This is the exact mechanism behind the accepted answer on Stack Overflow (92 votes, question from 2020, still the top result for this symptom): "statically optimized pages are hydrated without the route parameters, so the query is an empty object (). Next.js will populate the query after the page has been hydrated."

The official router object reference confirms the same thing for the query field: "It will be an empty object during prerendering if the page doesn't use Server-side Rendering. Defaults to {}". isReady is described right below it: "Whether the router fields are updated client-side and ready for use. Should only be used inside of useEffect methods and not for conditionally rendering on the server."

Reproduced locally on Next.js 15.5.0 (Pages Router, Docker, Node 22): a page under pages/[lang]/abc.js with no data-fetching function is marked ○ (Static) prerendered as static content by next build, and the HTML it ships embeds the probe already rendered:

text
id="probe">isReady=<!-- -->false<!-- --> query=<!-- -->{}

That line comes straight out of the static .html file next build writes for that route — curling the running page at /fr/abc?foo=bar returns the same markup, isReady=false, query={}, even though the request clearly carries lang=fr and foo=bar. The static file is reused for every value of lang; the client-side router fills query in afterwards, which is why the first one or two renders see it empty.

As a negative control, the same component mounted on pages/[lang]/ssr.js, which only adds export function getServerSideProps() { return { props: {} } }, renders on the very first response with isReady=true and a populated query, including the dynamic segment:

text
id="probe">isReady=<!-- -->true<!-- --> query=<!-- -->{&quot;foo&quot;:&quot;bar&quot;,&quot;lang&quot;:&quot;fr&quot;}

Forcing the page to go through the server on every request removes the static-optimization gap entirely — there is no cached HTML to reuse, so the route params are known before the first byte is sent.

Fix#

1. Guard every read with router.isReady#

jsx
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
 
export default function AbcPage() {
  const router = useRouter();
  const [lang, setLang] = useState(null);
 
  useEffect(() => {
    if (!router.isReady) return;
    setLang(router.query.lang);
  }, [router.isReady, router.query.lang]);
 
  if (lang === null) return null; // or a loading state
  return <div>Language: {lang}</div>;
}

isReady only flips to true once the client-side router has resolved the real path, so this is the condition the Pages Router docs name explicitly for this situation. The docs also warn not to use it "for conditionally rendering on the server" — it is a client-side signal, read it inside useEffect, not to branch what the server renders. As reproduced above, a page that opts out of static optimization with getServerSideProps already has isReady=true on its first response; a statically optimized page does not, which is the whole reason to guard with it in the first place.

2. Force server-side rendering when you cannot wait#

If the page genuinely needs the route parameter on the very first render — for a redirect decision, or data that must be in the initial HTML for SEO — add getServerSideProps, even an empty one:

jsx
export function getServerSideProps(context) {
  return { props: { lang: context.params.lang } };
}
 
export default function AbcPage({ lang }) {
  // lang is already defined on the first render
  return <div>Language: {lang}</div>;
}

This is the exact change reproduced above: adding getServerSideProps is what took the same component from isReady=false, query={} to isReady=true, query={"lang":"fr", …} on the first response, because it opts the page out of Automatic Static Optimization.

3. Before Next.js 10.0.5#

Versions before 10.0.5 did not have router.isReady. The accepted answer's older workaround compared router.asPath to router.route: they are equal until the real query is resolved, so checking router.asPath !== router.route inside useEffect told you query was safe to read. isReady is the documented replacement and the one to use on any current release.

App Router: useRouter does not have this problem#

The App Router's useRouter, imported from next/navigation, is a different hook from the Pages Router's. The official migration notes are explicit about what changed: "The query object has been removed and is replaced by useSearchParams()", and the dynamic segment itself comes from useParams(), not from the router object at all. There is no query property to be {} on first render, because it does not exist on that hook.

That does not mean the App Router has zero prerendering nuance for URL data — it has a different one. The useSearchParams reference documents it: "If a route is prerendered, calling useSearchParams will cause the Client Component tree up to the closest Suspense boundary to be client-side rendered." So instead of a value that is briefly {} and then fills in, the whole subtree waits behind a <Suspense> fallback until it can render with the real search params — wrap the component that calls useSearchParams() in its own <Suspense> boundary so the rest of the page can still be prerendered. The same page also notes that in an app still mixing the /pages and /app directories, useSearchParams() returns ReadonlyURLSearchParams | null, with null reserved for that migration case, "since search params cannot be known during prerendering of a page that doesn't use getServerSideProps" — the same root cause as the Pages Router symptom above, surfacing through a different API.

For reading query strings day to day in either router, see Next.js Query String Params: searchParams + useRouter, which lines up router.query, searchParams and useSearchParams() side by side.

Verify the fix#

With the isReady guard in place, add a log and confirm it only fires once, with the real value, instead of firing early with undefined:

jsx
useEffect(() => {
  if (!router.isReady) return;
  console.log('lang ready:', router.query.lang);
}, [router.isReady, router.query.lang]);

With getServerSideProps, the check is simpler: the prop is already correct on the very first render, so there is nothing to wait for — which is exactly what the negative control above showed on the live response, isReady=true and a populated query on the first byte.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Edge cases#

  • Shallow routing. router.push(url, undefined, { shallow: true }) updates query without re-running data fetching, but it still goes through the same isReady-gated update path — the guard above still applies after a shallow navigation, not only on first mount.
  • Catch-all routes ([...slug]) behave the same way: without a data-fetching function, the page is statically optimized and router.query.slug is undefined until isReady is true.
  • Middleware rewrites. Pages Router navigation has other edges beyond this one — a stale deployment or a rewrite that targets the current URL can throw a different error entirely; see Fix Invariant: attempted to hard navigate to the same URL if router.push/router.replace start throwing instead of returning undefined query values.
  • App Router Route Handlers have their own navigation pitfalls unrelated to this one, such as NEXT_REDIRECT logged in a Route Handler catch — worth knowing if the same project mixes Pages Router pages with App Router route handlers.

The underlying rule holds across both routers: a value derived from the URL is only safe to read once the framework tells you the route has actually resolved — router.isReady in the Pages Router, a <Suspense> boundary around useSearchParams() in the App Router. Reach for getServerSideProps (or an App Router equivalent that resolves params on the server) only when the first render genuinely cannot wait.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote