- Next.js
- Pages Router & Migration Strategy
- Migrating Navigation: next/router vs next/navigation
Migrating Navigation: next/router vs next/navigation
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
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!34export default function SearchPage() {5 const router = useRouter();67 // Monolithic hook returned everything:8 const { query, pathname } = router;9 const searchTerm = query.q as string;1011 const handleSearch = (newTerm: string) => {12 // Navigated via monolithic router:13 router.push({ pathname: '/search', query: { q: newTerm } });14 };1516 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
Can you use useParams() in Client Components?
Yes! `useParams()` from `next/navigation` returns dynamic segment values as an object in Client Components.
Why does useSearchParams require Suspense?
Because search parameters are unknown at build time; Suspense allows the static shell to prerender while streaming dynamic search params at request time.
