Skip to content

Next.js error

NextRouter Was Not Mounted

The error

Error: NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted

You are importing the Pages Router's useRouter inside the App Router. The two have different names, different shapes, and no compatibility layer.

There are two useRouter hooks. next/router belongs to the Pages Router; next/navigation belongs to the App Router. They are not the same API with a new import path - the shape changed - so this is the error that shows up when a component moves from one to the other and the import comes with it.

The replacement is three hooks, not one

The Pages Router's router object carried navigation, the current path, the query string and the dynamic parameters. The App Router splits them:

Pages RouterApp Router
router.push, router.replace, router.backuseRouter() from next/navigation
router.pathnameusePathname()
router.query (search params)useSearchParams()
router.query (dynamic segments)useParams()
'use client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
 
const router = useRouter();
const pathname = usePathname();
const params = useSearchParams();

All four are client-only hooks, so the file needs 'use client'.

Three differences that catch people out

router.events is gone. There is no routeChangeStart to subscribe to. Route change side effects are expressed as an effect on pathname and searchParams instead, which fires after navigation rather than before it. If you were using events to show a progress bar, the App Router's answer is a loading.tsx or a Suspense boundary - and that file is not a spinner.

router.isReady is gone, because there is nothing to wait for. Search params are available immediately.

useSearchParams opts the route out of static rendering unless you contain it. That is its own error and worth understanding before you reach for it: useSearchParams() should be wrapped in a suspense boundary.

Where it hides in a migration

The obvious cases are found immediately - a page throws and you fix the import. The ones that survive are in shared components: a <Breadcrumb> or a <LanguageSwitcher> in a component library, imported by both pages/ and app/. It works in the half of the application that has not migrated yet, so nothing looks broken until the App Router route renders it.

Search the repository for from 'next/router' and check each hit against which tree renders it. In a partly migrated codebase, the count of remaining imports is a reasonable measure of how far the migration actually got - which is usually further from finished than it looks.

If you inherited this codebase

An application with both routers, shared components importing from both, and no record of which pages moved is a specific and common state. What it needs first is a map rather than more migration - reading it before changing it is the part that gets skipped, and migration and rescue is that work done in order.

Related questions

Can the two routers coexist during a migration?
The routers can - a codebase can serve `pages/` and `app/` at the same time - but a single component cannot. A component rendered under `app/` must use `next/navigation`, and one under `pages/` must use `next/router`. Shared components are where this bites.
Why does it only fail at runtime?
Both imports resolve, so nothing fails at build. `useRouter` from `next/router` returns a router that was never mounted in an App Router tree, and the error appears the first time something reads it.