DEV Community

Cover image for Fix: NextRouter was not mounted
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Fix: NextRouter was not mounted

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 />);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// jest.setup.js or the test file itself
jest.mock('next/router', () => require('next-router-mock'));
Enter fullscreen mode Exit fullscreen mode
import { render } from '@testing-library/react';
import mockRouter from 'next-router-mock';
import { Nav } from './Nav';

test('renders nav', () => {
  mockRouter.push('/dashboard');
  render(<Nav />);
});
Enter fullscreen mode Exit fullscreen mode

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(),
  }),
}));
Enter fullscreen mode Exit fullscreen mode

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 },
};
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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

  1. grep -rn "from 'next/router'" src app __tests__ — every hit under app/ is the Cause 3 bug regardless of whether it currently throws.
  2. Run the failing test/story again with the mock or the corrected import; the error disappears without touching the component under test.
  3. 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.

Related Incidents


Originally published at https://www.iloveblogs.blog

Top comments (0)