Fix: NextRouter was not mounted
useRouter() throws this when it runs outside a real Pages Router tree — unit tests, Storybook, or an App Router migration. The three call-site fixes.
NextRouter was not mounted is thrown by useRouter() from next/router the
moment it runs in a tree that Next.js itself did not render. It is not a
router bug — it is useRouter() reading a React Context (RouterContext)
that only <App> populates, and three unrelated situations remove that
provider: unit tests, Storybook stories, and components moved to the App
Router. Each needs a different fix.
Why it throws#
next/router's useRouter() calls useContext(RouterContext) and throws
when the context value is null instead of silently returning undefined.
Next.js's internal <App> component wraps every page in
RouterContext.Provider during a real next dev/next build render, so the
error never appears in the browser for a page that Next.js actually served.
It appears the instant a component that calls useRouter() gets rendered by
something else — Jest, React Testing Library, Storybook, or an App Router
tree that has no Pages Router at all.
Cause 1: unit tests with Jest / React Testing Library#
// ❌ throws "NextRouter was not mounted"
import { render } from '@testing-library/react';
import { Nav } from './Nav'; // calls useRouter() internally
test('renders nav', () => {
render(<Nav />);
});render() mounts <Nav /> in a bare test DOM — no RouterContext.Provider
anywhere in the tree. The fix is next-router-mock, which drops in as a
same-shaped replacement for next/router and gives you a router object
without needing the real framework runtime:
npm install --save-dev next-router-mock// jest.setup.js or the test file itself
jest.mock('next/router', () => require('next-router-mock'));import { render } from '@testing-library/react';
import mockRouter from 'next-router-mock';
import { Nav } from './Nav';
test('renders nav', () => {
mockRouter.push('/dashboard');
render(<Nav />);
});If you cannot add a dependency, the cheaper stopgap is mocking next/router
directly with jest.mock, returning a plain object with the fields your
component reads (pathname, query, push, replace):
jest.mock('next/router', () => ({
useRouter: () => ({
pathname: '/dashboard',
query: {},
push: jest.fn(),
replace: jest.fn(),
}),
}));This is more brittle — every field your component starts using later needs to
be added by hand — which is why next-router-mock is the fix that survives
refactors.
Cause 2: Storybook stories#
Same root cause, different renderer: Storybook mounts the component in isolation, outside any Next.js page tree.
// .storybook/preview.js
import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtime';
import { createMockRouter } from './mocks/router';
export const parameters = {
nextRouter: { Provider: RouterContext.Provider },
};The maintained path here is the storybook-addon-next-router (or
storybook-addon-nextjs-router for newer Storybook majors) addon — it wraps
every story in the provider automatically and exposes router state as a
per-story parameter, so you configure it once instead of wrapping each story
manually.
Cause 3: the component actually lives in the App Router now#
This is the version that looks like a regression during a migration. If the
component that calls useRouter() from next/router was moved under app/,
there is no Pages Router <App> anywhere in the tree to provide the context —
the App Router does not use next/router at all. The fix is not a mock, it is
the correct import:
// ❌ Pages Router hook, throws in app/
import { useRouter } from 'next/router';
// ✅ App Router hook
import { useRouter } from 'next/navigation';next/navigation's useRouter() has a smaller API — no router.query
(read useParams() / useSearchParams() instead), and router.push() takes
no second as/options argument the way the Pages Router version did. A
component that only swaps the import but keeps reading router.query will
compile and then fail at runtime with query being undefined, which reads
like a second, unrelated bug — check every router.query reference when you
do this swap.
Verifying the fix#
grep -rn "from 'next/router'" src app __tests__— every hit underapp/is the Cause 3 bug regardless of whether it currently throws.- Run the failing test/story again with the mock or the corrected import; the error disappears without touching the component under test.
- If the component is shared between a Pages Router page and an App Router page, split it — one file cannot import both router hooks safely.
Testing a custom hook that calls useRouter, not a component#
The fixes above all wrap a rendered component — but a custom hook built on top of
useRouter() (something like useCurrentQuery() or useNavigateWithConfirm()) hits
the same "NextRouter was not mounted" error when tested with
@testing-library/react's renderHook(), and the fix looks slightly different because
there's no component tree to render the mock inside of directly. renderHook() takes
a wrapper option specifically for this: a component that supplies context around the
hook under test, which is exactly where the router mock belongs:
import { renderHook } from '@testing-library/react';
import mockRouter from 'next-router-mock';
import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtime';
import { useCurrentQuery } from './useCurrentQuery';
function wrapper({ children }: { children: React.ReactNode }) {
return (
<RouterContext.Provider value={mockRouter}>{children}</RouterContext.Provider>
);
}
test('reads the current query', () => {
mockRouter.push('/search?q=next');
const { result } = renderHook(() => useCurrentQuery(), { wrapper });
expect(result.current).toBe('next');
});Passing jest.mock('next/router', ...) at the top of the test file (as shown earlier
in this page) still works too and is simpler when the hook only needs push/replace;
the wrapper approach is worth reaching for once the hook reads several different
router fields and a plain mock object becomes tedious to keep in sync. Either way,
renderHook() without a router mock in place fails with the exact same error message
as render() does, for the exact same underlying reason — there's still no
RouterContext.Provider anywhere above the code under test. The broader test setup
this fits into — what to mock and what to run against a real database — is covered in
Testing Next.js + Supabase: Unit, Integration, RLS, E2E.
A different, unrelated error that looks similar: useSearchParams needs Suspense#
During an App Router migration it's easy to assume every next/navigation hook error
is a variant of "NextRouter was not mounted" — it is not. next/navigation's
useRouter() never throws that specific error; if a component using it renders inside
app/ with no Pages Router anywhere in the tree, it works, because next/navigation
doesn't rely on the Pages Router's RouterContext at all. What does throw during
static generation, and gets confused with this bug because both surface while touching
router hooks, is useSearchParams() requiring a <Suspense> boundary around any
component that calls it — a separate constraint tied to how the App Router handles
dynamic rendering, not to context being missing:
// ❌ throws "useSearchParams() should be wrapped in a suspense boundary"
export default function SearchBar() {
const params = useSearchParams();
return <input defaultValue={params.get('q') ?? ''} />;
}// ✅ wrap the component that reads it
import { Suspense } from 'react';
export default function Page() {
return (
<Suspense>
<SearchBar />
</Suspense>
);
}This is a build-time constraint specific to useSearchParams(), not usePathname() or
useRouter() from next/navigation, both of which render fine without a Suspense
wrapper. Full coverage of this exact error and why the boundary has to sit above the
component that calls the hook, not around the value itself, is in
Fix: Missing Suspense Boundary with useSearchParams in Next.js.
Further reading: Next.js Query String Params: searchParams + useRouter.
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Related fixes & guides
- Fix: Next.js middleware not triggering
- Fix: Event handlers cannot be passed to Client props
- cookies() Returns a Promise in Next.js 15/16: Fix
- Next.js 16 Removed next lint: ESLint 9 Migration
- Next.js 15 Middleware: Auth, Rate Limiting & Edge Guide
- Next.js Server Actions with Supabase: Complete Guide
- Fix: useRouter query undefined on first render
- Fix Next.js revalidatePath Not Working in Server Actions
- Fix "cookies() should be awaited" Error in Next.js 15
- Fix Invariant: attempted to hard navigate to the same URL

