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.
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.
// 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:
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:
id="probe">isReady=<!-- -->true<!-- --> query=<!-- -->{"foo":"bar","lang":"fr"}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#
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:
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:
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 })updatesquerywithout re-running data fetching, but it still goes through the sameisReady-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 androuter.query.slugisundefineduntilisReadyistrue. - 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.replacestart throwing instead of returningundefinedquery 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
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Next.js Query String Params: searchParams + useRouter
If `router.query` examples keep breaking on you, the problem is usually that you're mixing App Router and Pages Router APIs. Here is the exact fix for each case.
Next.js Redirect from / to another page
If your redirect works only after render, flickers, or fires in the wrong place, you are probably using the wrong redirect API for the job. Here is the exact mapping.
Fix Invariant: attempted to hard navigate to the same URL
Reproduced on Next.js 13.1.0, 13.2.0 and 16.4.3: why a NextResponse.next() middleware broke the 404 page, why the same invariant still fires after a redeploy, and the catch that stops it.


