Next.js Curriculum
MediumPages Router Migration100% Free25 min read

Migrating Navigation: next/router vs next/navigation

The 30-Second Interview Answer

In Pages Router, next/router provided a monolithic useRouter() hook returning pathname, query, asPath, and router events. In App Router, next/navigation decomposes these responsibilities into specialized hooks: useRouter() for programmatic navigation, usePathname() for the current URL path, useSearchParams() for query parameters, and useParams() for dynamic route segments.

Learning Objectives

Deconstruct the monolithic useRouter hook into App Router modular hooks.

Migrate router events (routeChangeStart/routeChangeComplete) to React Suspense and page effects.

Read and update URL search params immutably using URLSearchParams.

Avoid common "NextRouter was not mounted" runtime errors.

Prerequisites & Background Knowledge
Pages Router next/routerApp Router next/navigationURLSearchParams API

Why Interviewers Care

The most frequent runtime crash during migration occurs when developers import useRouter from next/router inside App Router components.

The Challenge

Compare next/router with next/navigation. Show how to refactor a component reading route params, query strings, and navigating programmatically from Pages Router to App Router.

Code Architecture & Implementation

1// pages/search.tsx (Legacy Pages Router)
2import { useRouter } from 'next/router'; // FROM NEXT/ROUTER!
3
4export default function SearchPage() {
5 const router = useRouter();
6
7 // Monolithic hook returned everything:
8 const { query, pathname } = router;
9 const searchTerm = query.q as string;
10
11 const handleSearch = (newTerm: string) => {
12 // Navigated via monolithic router:
13 router.push({ pathname: '/search', query: { q: newTerm } });
14 };
15
16 return <div>Query: {searchTerm}</div>;
17}

Architectural Explanation

In Pages Router, useRouter was an overloaded object that caused unnecessary re-renders: whenever any query parameter changed, every component using useRouter re-rendered. In App Router, next/navigation separates concerns: usePathname() only re-renders when the path changes; useSearchParams() only re-renders when query parameters change; and useRouter() provides imperative methods (push, replace, refresh, back) without causing re-renders when URLs change.

Common Mistakes & Anti-Patterns

Importing `useRouter` from `next/router` in App Router components, causing instant runtime crashes.

Expecting `router.events` to exist in `next/navigation` (route change events are deprecated; use Suspense and usePathname).

Forgetting to wrap Client Components using `useSearchParams` in `<Suspense>`, breaking static builds.

Interview Follow-Up Traps

The interviewer asks: "How do you listen to route changes in App Router without router.events.on('routeChangeStart')?" Explain that you monitor pathname changes with `usePathname()` inside a `useEffect`, or use libraries like `next-nprogress-bar` wrapped around `<Link>`.

Frequently Asked Questions

Does useRouter() in next/navigation have shallow routing?

Shallow routing is replaced by standard `window.history.pushState` or updating search params without triggering new RSC data fetches.

Yes! `useParams()` from `next/navigation` returns dynamic segment values as an object in Client Components.

Because search parameters are unknown at build time; Suspense allows the static shell to prerender while streaming dynamic search params at request time.