Skip to the solution
EasyFrontend9 min read

How does client-side navigation work in Next.js (next/link and useRouter)?

986 views

The problem

Question presented to candidate: "In a Next.js app, what actually happens when a user clicks a <Link>, compared to a normal <a> tag? And when would you use useRouter instead?"

What a strong answer should cover:

  • <Link> renders a real <a href> but intercepts the click: no new document is loaded, the URL is updated with the History API, and only the route segments that changed are re-rendered; shared layouts and their state stay.
  • The data for the new route is the RSC payload, fetched from the server or taken from a prefetch.
  • Links are prefetched in production when they enter the viewport: static routes fully, dynamic routes only partly (the shared parts and any loading.tsx).
  • useRouter() from next/navigation (not next/router) does the same from code: push, replace, back, forward, prefetch, refresh; it only works in Client Components.
  • router.refresh() re-renders Server Components for the current route without losing client state; it does not invalidate server caches.
  • A plain <a>, or a link to a route under a different root layout, is a full page load.

Clarifying questions expected:

  • "Does the navigation happen after an event, like a form submit?" β€” that is the case for useRouter; otherwise prefer <Link>.
  • "Are the target routes static or dynamic?" β€” decides how much prefetching helps and whether a loading.tsx is needed.

Code / implementation expected: Yes β€” a nav with active links, a Link with prefetch={false} and replace, and a button that calls router.refresh() and router.push().

  • nextjs
  • navigation
  • next-link
  • prefetch

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 8 sections

Read the code

Active links, prefetch control, replace, refresh and push
// app/components/NavLink.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";

// Active-link styling: usePathname() re-renders this on every client navigation.
export default function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
  const active = usePathname() === href;
  return (
    <Link href={href} aria-current={active ? "page" : undefined} className={active ? "active" : ""}>
      {children}
    </Link>
  );
}

// app/layout.tsx
import Link from "next/link";
import NavLink from "./components/NavLink";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <nav>
          <NavLink href="/">Home</NavLink>
          <NavLink href="/posts">Posts</NavLink>
          {/* No prefetch for a rarely used, expensive route */}
          <Link href="/reports" prefetch={false}>Reports</Link>
          {/* A plain <a> is a full page load: no prefetch, layout state is lost */}
          <a href="/legacy">Legacy app</a>
        </nav>
        {children}
      </body>
    </html>
  );
}

// app/components/SaveButton.tsx
"use client";
import { useRouter } from "next/navigation";

export default function SaveButton({ slug }: { slug: string }) {
  const router = useRouter();
  async function save() {
    await fetch(`/api/posts/${slug}`, { method: "POST" });
    router.refresh(); // re-run Server Components for this route; client state is kept
    router.push(`/posts/${slug}?saved=1`, { scroll: false }); // new history entry, keep scroll
  }
  return <button onClick={save}>Save</button>;
}

// app/posts/[slug]/page.tsx
import Link from "next/link";
import SaveButton from "../../components/SaveButton";

export default async function Post({ params }: PageProps<"/posts/[slug]">) {
  const { slug } = await params;
  return (
    <article>
      <h1>{slug}</h1>
      <SaveButton slug={slug} />
      {/* replace: switch tabs without adding history entries */}
      <Link href={`/posts/${slug}?tab=comments`} replace scroll={false}>Comments</Link>
    </article>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 5 of 129 in Next.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Next.js questions
Up nextEasy

How does file-based routing work in the Next.js App Router?

Continue