"NextRouter was not mounted" in Next.js: Fix for the App Router, Tested
This error means a component imported useRouter from next/router inside the app folder. next/router is the Pages Router's hook; the App Router never sets it up, so the hook has nothing to read. The fix is to import useRouter from next/navigation instead, and get the query, path and params from their own hooks.
I reproduced it on Next.js 16.4.4 with a client page in app/:
"use client";
import { useRouter } from "next/router"; // wrong package for app/
export default function Page() {
const router = useRouter();
return <button onClick={() => router.push("/")}>Go home</button>;
}next dev returned a 500 for the page and logged:
⨯ Error: NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mountednext build failed at the same line, because the page is prerendered:
Error occurred prerendering page "/r-old".
Error: NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted
> 4 | const router = useRouter();
Export encountered an error on /r-old/page: /r-old, exiting the build.The fix: next/navigation
"use client";
import { useRouter, usePathname, useSearchParams, useParams } from "next/navigation";
export default function Nav() {
const router = useRouter();
const pathname = usePathname(); // "/r-new/42"
const searchParams = useSearchParams(); // searchParams.get("q") -> "shoes"
const params = useParams<{ id: string }>(); // params.id -> "42"
return <button onClick={() => router.push("/")}>Go home</button>;
}At /r-new/42?q=shoes this rendered pathname "/r-new/42", q "shoes" and id "42". The component using useSearchParams has to sit inside a Suspense boundary, or the build fails with a different error; that one is covered in useSearchParams should be wrapped in a Suspense boundary.
router.query, asPath and events are gone
Changing the import isn't always enough, because the App Router's router object is smaller. In the browser, Object.keys on it gave back, refresh, push, replace, prefetch and forward, plus a few internal and dev-only entries. query, asPath and events were undefined, and TypeScript says so at build time:
error TS2339: Property 'query' does not exist on type 'AppRouterInstance'.Here is what replaces each one:
- router.query (search part) → useSearchParams(), then searchParams.get("q").
- router.query (dynamic segments like [id]) → useParams(), or the params prop on the page.
- router.pathname / router.asPath → usePathname() for the path; add useSearchParams().toString() if you need the query string too.
- router.events (routeChangeStart and so on) → no direct replacement. Watch usePathname() and useSearchParams() in a useEffect to react after a navigation.
- router.isReady → not needed. In the Pages Router my test page rendered query {} and isReady false on the server, and only got q after hydration; the App Router hooks had the value on the first render.
- router.push, replace, back, refresh, prefetch → same names on the new router. push and replace take a URL string. The Pages Router's object form ({ pathname, query }) is a type error ("not assignable to parameter of type 'string'"), and with the check bypassed, clicking did nothing: the URL didn't change. Build the string yourself, for example `/search?${new URLSearchParams({ q })}`.
"invariant expected app router to be mounted"
This is the opposite problem: a component uses useRouter from next/navigation, but renders somewhere with no App Router around it. That usually means a unit test (Jest, Vitest) or Storybook. I rendered components with react-dom/server outside Next to see what each hook does there:
useRouter() from next/navigation throws: invariant expected app router to be mounted
usePathname() returns null
useSearchParams() returns null
useRouter() from next/router throws: NextRouter was not mountedSo a component that only reads the path or search params won't crash in a test, but will get null. One that calls useRouter will throw. Wrapping it in the router context with a stub router fixed it:
import { AppRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";
const router = { push() {}, replace() {}, refresh() {}, back() {}, forward() {}, prefetch() {} };
render(
<AppRouterContext.Provider value={router}>
<Nav />
</AppRouterContext.Provider>
);The import path is internal to Next, so it can move between versions. The other usual approach is to mock the whole module in the test, for example jest.mock("next/navigation", () => ({ useRouter: () => ({ push: jest.fn() }), usePathname: () => "/" })), which doesn't depend on Next's internals.
A pages folder makes the hooks nullable
If the project still has a pages folder next to app (common while migrating), the same correct code can fail type checking:
app/r-new/[id]/nav.tsx: error TS18047: 'searchParams' is possibly 'null'.
app/r-new/[id]/nav.tsx: error TS18047: 'params' is possibly 'null'.
Failed to type check.With a pages folder, Next adds this line to next-env.d.ts, which switches the next/navigation types to versions that can return null:
/// <reference types="next/navigation-types/compat/navigation" />Removing the pages folder made the line go away and the build pass. If you need both, handle null (searchParams?.get("q")). The types match what happens: in a Pages Router page, useSearchParams() and useParams() from next/navigation returned null during server rendering in my test, and the real values after hydration. useRouter from next/navigation did work there: the push call navigated.
Quick checklist
- In app/, every useRouter import should come from next/navigation. Search the project for from "next/router".
- A shared component used by both app/ and pages/ should use next/navigation and handle null from usePathname and useSearchParams.
- Replace router.query with useSearchParams() and useParams(), and router.asPath with usePathname().
- In tests and Storybook, give components a router context or mock next/navigation.
Related
- useSearchParams should be wrapped in a Suspense boundary
- params should be awaited in Next.js
- You're importing a component that needs useState
- Next.js development