cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Linking and navigating

See in real Chrome what Link prefetches and what a click asks for, mark active links, navigate with useRouter, and handle pending links, scroll and focus.

By the end, you can
  • 01
    Link pages, and know what they fetchUse Link, and tell what Next.js prefetches when a link is on the screen and what a click still asks for.
  • 02
    Navigate from codeMark the current page's link with usePathname, and use push, replace, refresh and back from useRouter.
  • 03
    Handle the moments around a clickShow a pending hint with useLinkStatus, stop a navigation with onNavigate, and know where scroll and focus go.

A click that swaps only the page

In Layouts and pages, a tea layout kept its counter while the visitor clicked from Green tea to Mint tea. That worked because the menu used Link, from next/link. This lesson is about Link itself, and everything around a click: what Next.js loads before it, what the click asks for, how to navigate from your own code, and where the scroll and the keyboard focus go afterwards.

Here is the tea shop with a Link to /teas. The teas page has a part, the stock, that the server makes for each request and that takes a moment. A click needs the page's JavaScript running, so this example was also opened in real Chrome against next start, and Chrome took four steps: open /, click Teas, wait for the stock, click Home. Open the Steps tab and pick each step: you see the page after it, and the requests the page made during it.

The tea shop: a Link to a page with a slow partNext.js
app/teas/
app/layout.tsx
import Link from "next/link"; export const metadata = { title: "Tea shop" }; export default function RootLayout({ children }: LayoutProps<"/">) {  return (    <html lang="en">      <body>        <nav>          <Link href="/">Home</Link> <Link href="/teas">Teas</Link>        </nav>        {children}      </body>    </html>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/teasTea shop
Home Teas
Our teas

Green tea and mint tea.

Checking stock…

Focus
the link “Teas”
Scrolled
0 px down

Requests the page made during this step (1), without the files it loaded

  1. GET/teas200text/x-component
    rsc
    1
    next-router-state-tree
    %5B%22%22%2C%7B%22children%22%3A%5B%22teas%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2C%22refetch%22%2C49314%5D%7D%2Cnull%2Cnull%2C49250%5D%7D%2Cnull%2Cnull%2C49234%5D
    next-url
    /

Line 10 of app/layout.tsx has two links. In the Network tab, the HTML of / has them as plain <a href="/"> and <a href="/teas"> elements, so they work even before any JavaScript runs. Once it runs, Next.js handles their clicks. When the new page has the same root layout as the current one (the layout that renders <html> and <body>), as every page in this shop does, it changes the address and swaps only the page part, without loading a new HTML page. That's a client-side navigation.

The exception: an app can have more than one root layout, one per group of pages. A Link to a page under a different root layout loads a whole new HTML page, because the <html> itself changes. Route groups and private folders recorded one. Every example in this lesson has a single root layout.

Now read the steps:

  1. Open /. One document request, for the HTML page. Then, before anyone clicked, fetches for /teas with the header next-router-prefetch. That's prefetching: loading a linked page in the background, before the click. What it loads is the page's static shell: the parts that don't wait for a request, like the heading and the Checking stock… fallback.
  2. Click Teas. The address is /teas, and the page shows Our teas and Checking stock…. There's one fetch, without the prefetch header: the router asking for the part it didn't have, the stock. Its answer is text/x-component, an RSC payload: the data React uses to draw what the Server Components rendered. (How Next.js works showed one.)
  3. Wait for the stock. In stock: 12 boxes takes the place of Checking stock…, from the same request.
  4. Click Home. No requests at all: the home page was prefetched when the page opened, and nothing on it is made for each request.
Three stages, left to right. One: a browser window for slash shows the Teas link, and an arrow labeled prefetch brings a card, Our teas plus Checking stock, the static shell of slash teas. Two: the visitor clicks, and the window for slash teas shows that shell at once. Three: a request brings the last card, In stock: 12 boxes, which takes the place of Checking stock.1. The link appears/Teas linkshell of /teasprefetched2. The click/teasOur teasChecking stock…the shell, shown at once3. The rest arrives/teasOur teasIn stock: 12 boxesone request, for the stock
With partial prefetching, a Link on the screen loads the linked page's static shell early. After the click, the shell shows with nothing new to download for it, and the router asks only for the part made for each request: here, the stock.
Real-life analogyA shop assistant who looks where you look

In a small tea shop, an assistant watches which shelves you look at, and fetches the gift boxes for them from the back before you ask. When you point at one, she already has the box; she only fills it with today's tea, which she has to check in the stockroom. You never leave the shop to buy something else.

In real life: The shelves you can see from where you stand
In Next.js: The links on the screen
In real life: The assistant fetching the empty gift boxes for those shelves, before you ask
In Next.js: Prefetching each linked page's static shell
In real life: You point at a shelf, and the box is already in her hands
In Next.js: The click shows the shell with nothing new to download for it
In real life: She fills the box with today's tea, which only the stockroom knows
In Next.js: The router asks the server for the part made for each request
In real life: You stay in the same shop: the counter and the signs don't change
In Next.js: The layout stays; only the page changes
The main idea

Link is an <a> that Next.js turns into a client-side navigation when both pages share a root layout. While the link is on the screen, Next.js prefetches the linked page's static shell. On the click, it shows that shell, asks the server only for what's made for each request, and swaps the page part while the layouts stay.

Prefetching what's on the screen

New Next.js 16 projects turn on partial prefetching in next.config.ts, with partialPrefetching: true; Next.js 16.3 added it, and 16.4 made it part of every new app. With it, a Link prefetches the linked route's App Shell: the parts of the page that don't depend on the link's address. When the route reads nothing about the visitor, that's its static shell, made during the build, the same one a direct visit gets as HTML; when it reads cookies, the App Shell is rendered for each visitor's session (Instant navigations and partial prefetching records both). One App Shell serves every link to that route. The parts made for each request, like the stock, stream in after the click.

Compare three ways to link to /teas. Each version took the same steps in Chrome. Pick a version, then look at the requests of step 1 (opening /) and step 2 (the click).

Playground: what does the router fetch, and when?
The tea shop, with this link to /teasNext.js
app/teas/
app/layout.tsx
import Link from "next/link"; export const metadata = { title: "Tea shop" }; export default function RootLayout({ children }: LayoutProps<"/">) {  return (    <html lang="en">      <body>        <nav>          <Link href="/">Home</Link> <Link href="/teas">Teas</Link>        </nav>        {children}      </body>    </html>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
Home Teas
Tea shop

Fresh tea, sent to your door.

Focus
nothing: the page itself
Scrolled
0 px down

Requests the page made during this step (4), without the files it loaded

  1. GET/200a whole page
  2. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /
  3. GET/teas200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /
  4. GET/teas200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /teas/__PAGE__
    next-url
    /
Try it yourself
Link

When the page opened, the router prefetched /teas in the background: its static shell, the parts that don't wait for a request. So after the click the page shows Our teas and Checking stock… with nothing new to download for them, and the router asks the server only for the stock.

Each version was built with next build from Next.js 16.4.3, run with next start, and clicked through in real Chrome: open /, click Teas, wait for the stock. Only line 10 of app/layout.tsx changes. Reset goes back to the first version.

With Link, step 1 has the prefetches for /teas, and the click asks only for the stock. With prefetch={false}, step 1 has no request for /teas; the router asks for the page at the click. With a plain <a>, there's no prefetch and no client-side navigation: the click is a document request, a whole new page.

Next.js doesn't prefetch every link on the page at once. It prefetches a link when the link enters the viewport, the part of the page on the screen. This home page has a Gift boxes link far down, below a tall empty <div> that stands in for a long page:

A link far down the pageNext.js
app/gift-boxes/teas/
app/page.tsx
import Link from "next/link"; export default function HomePage() {  return (    <main>      <h1>Tea shop</h1>      <p>Fresh tea, sent to your door.</p>      <div style={{ height: 1600 }} />      <Link href="/gift-boxes">Gift boxes</Link>    </main>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
Home Teas
Tea shop

Fresh tea, sent to your door.

Gift boxes
Focus
nothing: the page itself
Scrolled
0 px down

Requests the page made during this step (4), without the files it loaded

  1. GET/200a whole page
  2. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /
  3. GET/teas200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /
  4. GET/teas200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /teas/__PAGE__
    next-url
    /

Step 1 prefetches / and /teas, the links in the layout, but not /gift-boxes. Step 2 scrolls to the bottom, and the prefetch for /gift-boxes appears. Step 3 clicks it, with no request at all: the page is prerendered as static content, so its shell is the whole page.

A tall page drawn as a column: the Home and Teas links at the top, a long empty space, and the Gift boxes link at the bottom. Left, a window frame covers the top of the page: Home and Teas are inside it and marked prefetched; Gift boxes is far below and marked not yet. Right, the frame has moved down to the bottom: Gift boxes is inside it and marked prefetched now.Just openedscreenHomeTeasGift boxesnot yet: far belowprefetchedScrolled downHomeTeasscreenGift boxesalready prefetched
Next.js prefetches a Link when it enters the viewport, the part of the page on the screen. At first only Home and Teas are there. Scroll down, and Gift boxes is prefetched too.

Three more facts from the Link reference on nextjs.org:

  • Production only. Prefetching happens after next build and next start; in next dev, links don't prefetch, so test this in a production build.
  • Hover. A prefetch doesn't last forever. If what was prefetched has expired by the time the visitor hovers over the link, Next.js prefetches it again.
  • Turning it off. prefetch={false} turns off both: no prefetch when the link enters the viewport, and none on hover. A list of hundreds of links, like an endless list of orders, can use it so the browser doesn't fetch pages nobody will open.

Navigating from code with useRouter

Sometimes your code, not a link, decides where to go: after a button press, or when something finishes. For that, Client Components call useRouter from next/navigation. The router it returns has a few methods:

  • router.push(href) navigates to href, adding an entry to the browser's history, like a Link click.
  • router.replace(href) navigates too, but replaces the current history entry, so Back skips the page you left.
  • router.refresh() asks the server for the current page again, and merges the new output of its Server Components into the page, keeping the state in the browser.
  • router.back() goes to the previous history entry, like the browser's Back button.

The cart and checkout below use all four. Chrome took eight steps; read the code first, then the steps.

A cart and a checkout that navigate from codeNext.js
app/cart/checkout/thanks/
app/cart/cart-buttons.tsx
"use client"; import { useState } from "react";import { useRouter } from "next/navigation"; export default function CartButtons() {  const router = useRouter();  const [note, setNote] = useState("");   function addMint() {    document.cookie = "cart=mint; path=/";    router.refresh();  }   return (    <div>      <label>        Note for the driver <input value={note} onChange={(event) => setNote(event.target.value)} />      </label>      <p>        <button onClick={addMint}>Add a mint tea</button> <button onClick={() => router.push("/checkout")}>Checkout</button>      </p>    </div>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/cartTea shop
Home Cart
Your cart

In your cart: Mint tea

Add a mint tea Checkout

Focus
the button “Add a mint tea”
Scrolled
0 px down

Requests the page made during this step (3), without the files it loaded

  1. GET/cart200text/x-component
    rsc
    1
    next-router-state-tree
    %5B%22%22%2C%7B%22children%22%3A%5B%22cart%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%2C162%5D%7D%2Cnull%2Cnull%2C98%5D%7D%2Cnull%2C%22refetch%22%2C82%5D
  2. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /__PAGE__
    next-url
    /cart
  3. GET/cart200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /cart/__PAGE__
    next-url
    /cart

In app/cart/cart-buttons.tsx, line 7 gets the router. addMint stores a cookie on line 11 and calls router.refresh() on line 12. In step 3, the cart says In your cart: Mint tea: the server ran CartItem again, and it read the new cookie. The note typed in step 2, Leave it at the door, is still in its field, because note is state in the browser and the refresh kept it. The step's requests show one fetch for /cart, not a document: no page reload.

Line 21's Checkout button calls router.push("/checkout"), and step 4 is on /checkout. It made one fetch: nothing had prefetched the checkout, because no Link pointed to it. (Your code can warm a page up with router.prefetch(href).) In app/checkout/checkout-buttons.tsx, Back to cart calls router.back(), and step 5 is on /cart again, with the note still there. Place order calls router.replace("/thanks"). Then step 8 presses the browser's Back button, and the address is /cart, not /checkout: the replace swapped the checkout's entry in the history for /thanks.

Only send router.push addresses you trust

Next.js's docs warn: never pass an address from a visitor, like a query value, to router.push or router.replace without checking it. A javascript: address would run code in your page.

Pending links and onNavigate

Next.js 15.3 added two tools for the moment of a click. The first is useLinkStatus, a hook from next/link. A component inside a Link calls it, and gets pending: true after the click, while the router waits for the page and the address hasn't changed yet, and false otherwise. Use it for a small hint that the click was heard.

To see it, the Orders link below doesn't prefetch, and Chrome's network was made slow before the click, the way a weak phone signal is.

Playground: when does the pending hint show?
The tea shop, with this Orders linkNext.js
app/orders/
app/layout.tsx
import Link from "next/link";import PendingHint from "./pending-hint"; export const metadata = { title: "Tea shop" }; export default function RootLayout({ children }: LayoutProps<"/">) {  return (    <html lang="en">      <body>        <nav>          <Link href="/">Home</Link>{" "}          <Link href="/orders" prefetch={false}>            Orders            <PendingHint />          </Link>        </nav>        {children}      </body>    </html>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
Home Orders (opening…)
Tea shop

Fresh tea, sent to your door.

Focus
the link “Orders (opening…)”
Scrolled
0 px down

Requests the page made during this step (1), without the files it loaded

  1. GET/orders200text/x-component
    rsc
    1
    next-router-state-tree
    %5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%2C514%5D%7D%2Cnull%2Cnull%2C530%5D
    next-url
    /
Try it yourself
prefetch={false}

On a slow network, the click on Orders waits for the server. Until the page arrives, useLinkStatus returns pending: true, and the hint shows (opening…) next to the link while the old page stays on the screen.

Both versions were built with next build from Next.js 16.4.3, run with next start, and clicked in real Chrome. After opening /, Chrome's network was made slow, then Orders was clicked. Reset goes back to the first version.

In the first version, app/pending-hint.tsx calls useLinkStatus() and shows (opening…) while pending is true; line 14 of the layout puts it inside the Orders link. Step 3 caught it: the address is still /, the old page is still on the screen, and the link says Orders (opening…). In step 4, the orders page is there and the hint is gone. In the second version, the link prefetched the page while the network was still fast, and the click needs no request: no pending moment, no hint. So the hook helps most when a link can't prefetch, or the prefetch hasn't finished.

The second tool is onNavigate, a prop of Link. Next.js calls it when the click starts a client-side navigation, with an event whose preventDefault() stops it. The feedback form uses it so visitors don't lose what they wrote:

A Link that won't leave unsent feedback behindNext.js
app/feedback/teas/
app/feedback/feedback-form.tsx
"use client"; import Link from "next/link";import { useState } from "react"; export default function FeedbackForm() {  const [text, setText] = useState("");  const [warning, setWarning] = useState("");  return (    <div>      <label htmlFor="feedback">Your feedback</label>      <textarea id="feedback" value={text} onChange={(event) => setText(event.target.value)} />      <p>        <Link          href="/"          onNavigate={(event) => {            if (text) {              event.preventDefault();              setWarning("Send or clear your feedback before you leave.");            }          }}        >          Back to the shop        </Link>      </p>      {warning && <p>{warning}</p>}    </div>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/feedbackTea shop
Home Teas
Tell us what you think
Lovely mint tea

Back to the shop

Send or clear your feedback before you leave.

Focus
the link “Back to the shop”
Scrolled
0 px down

No requests during this step: the page already had what it needed

Lines 16 to 21 are the handler. When text isn't empty, line 18 stops the navigation and line 19 shows a warning. In step 3, after Lovely mint tea was typed, the click on Back to the shop left the address at /feedback, with the warning under the link. In step 4 the field was cleared, and in step 5 the same click went to /. onNavigate isn't onClick: the docs say it doesn't run when the click opens a new tab, or for a link to another website, because those aren't client-side navigations.

Scroll and focus after a navigation

A whole page load starts at the top of the new page. A client-side navigation keeps the same window, so Next.js decides where the scroll goes. Its rule: if the top of the new page is already in view, it doesn't scroll; if not, it scrolls to it. The teas page below is long, with the Green tea link at the bottom, and the window is 640 pixels tall. Chrome scrolled down, then clicked.

Playground: where does the page scroll to?
The long tea pages, with this Green tea linkNext.js
app/teas/green/
app/teas/page.tsx
import Link from "next/link"; export default function TeasPage() {  return (    <main>      <h1>Our teas</h1>      <div style={{ height: 1600 }} />      <Link href="/teas/green">Green tea</Link>    </main>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/teas/greenGreen tea
Home Teas
Green tea

Light and fresh. $4 a box.

Focus
nothing: the page itself
Scrolled
0 px down
Announced to screen readers
Green tea

No requests during this step: the page already had what it needed

Try it yourself
Link

The visitor scrolled down to the Green tea link. After the click, the new page's top was out of view, so Next.js scrolled the window to the top: 0 px.

Both versions were built with next build from Next.js 16.4.3, run with next start, and clicked in real Chrome in a 1024 × 640 window: open /teas, scroll down, click Green tea. Reset goes back to the first version.

Before the click, step 2 is 1092 px down, as far as the page goes. After it, with a plain Link, step 3 is at 0 px: the top of the Green tea page was out of view, so Next.js scrolled to it. With scroll={false}, step 3 is still at 1092 px, and the heading is off the screen. router.push(href, { scroll: false }) does the same from code. Use it when the new page should open where the visitor was, like a list whose filter changed.

Left, a browser window scrolled far down /teas shows the Green tea link at the bottom. Two arrows lead right. The top arrow, labeled Link, goes to a window showing the top of the Green tea page with its heading: scrolled to 0. The bottom arrow, labeled scroll={false}, goes to a window showing an empty part far down the Green tea page: the heading is out of view./teasGreen tea linkscrolled down, then a clickLink/teas/greenGreen teaat the top: 0 pxscroll={false}/teas/greenempty spacestill far down
The same click, two ways. A Link scrolls the window to the new page when the page's top is out of view. With scroll={false}, the window stays as far down as it was.

Now the keyboard focus: the element that gets the keys the visitor presses, like Tab or Enter. Next.js 16.4.3 doesn't move it after a navigation; a comment in its scroll handler says it leaves focus untouched on purpose. So the recordings show two cases. In step 3 of the first version, the clicked Green tea link was part of the old page, which Next.js hid, so nothing has focus. In step 4, the clicked link, Teas, is in the layout, which stayed, so it still has focus.

Screen reader users still hear that the page changed. Next.js adds a hidden route announcer to every page: a part that screen readers read out, which says the new page's title after a navigation, or its <h1> if it has no title. The Green tea page sets its own title, and step 3 shows the announcement Green tea; after the click on Teas, it says Tea shop. You can read it in app-router-announcer.tsx.

Choosing how to navigate

Each row is one way to move the visitor, when it fits, and what it does.

Ways to navigate
WriteWhen it fitsWhat happens
<Link href="/teas">A visitor clicks something that goes to another page of your app.Prefetches. To a page under the same root layout, swaps only the page and keeps layouts and their state; under another root layout, loads a whole new page.
router.push("/checkout")Your code decides where to go, after something happens, like a button press.A client-side navigation too, but nothing was prefetched unless you call router.prefetch.
router.replace("/thanks")The page you leave shouldn't come back with Back, like a finished checkout.Swaps the current history entry instead of adding one.
router.refresh()The server's data changed, and the page should show it without leaving.Asks the server for the page again, and keeps the state in the browser.
<a href="https://…">The address is another website.A whole new page load: nothing of your app is kept.

A rule of thumb: if the visitor clicks something that goes somewhere, it's a Link. If your code decides, it's useRouter. Moving the visitor on the server, before a page renders, is a redirect; the lesson on redirects comes later in the Routing and navigation stage: Redirects. Sort these:

Link or useRouter?
  • The shop's menu: Home, Teas and Cart.
  • A list of teas, each going to its own page.
  • A Checkout button that goes to /checkout when it's pressed.
  • After Place order, go to /thanks, and don't let Back return to the checkout.
  • After a cookie changes in the browser, show the cart the server makes from it.
  • A footer link to the About page.
Try it yourself
0 of 6 correct

Sort each part of the tea shop: is it a Link, or code that calls useRouter?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "Link doesn't render a real link." No. It renders an <a href>, as the recorded HTML shows.
  • "A Link prefetches the whole page." No. With partial prefetching, it prefetches the static shell. The stock came only after the click.
  • "Every link on the page is prefetched as soon as it loads." No. Only links in or near the viewport; Gift boxes was prefetched after the scroll.
  • "Prefetching works in next dev." No. It runs only in production, as the docs say. Test it with next build and next start.
  • "router.refresh() reloads the page." No. It fetches the server's new output and keeps the state in the browser: the note stayed.
  • "Next.js moves focus to the new page." No. It leaves focus alone, and the route announcer tells screen readers about the new page.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upRead a Link's HTML

What does line 10's <Link href="/teas">Teas</Link> become in the HTML that the server sends?

Starter codeTSX
import Link from "next/link";

export const metadata = { title: "Tea shop" };

export default function RootLayout({ children }: LayoutProps<"/">) {
  return (
    <html lang="en">
      <body>
        <nav>
          <Link href="/">Home</Link> <Link href="/teas">Teas</Link>
        </nav>
        {children}
      </body>
    </html>
  );
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind when the router asks

    With this layout, when does the router first ask the server for /teas?

    Starter codeTSX
    import Link from "next/link";
    
    export const metadata = { title: "Tea shop" };
    
    export default function RootLayout({ children }: LayoutProps<"/">) {
      return (
        <html lang="en">
          <body>
            <nav>
              <Link href="/">Home</Link> <Link href="/teas" prefetch={false}>Teas</Link>
            </nav>
            {children}
          </body>
        </html>
      );
    }
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticePredict the active link

      On /teas/green, which link does this menu mark with aria-current="page"?

      Starter codeTSX
      "use client";
      
      import Link from "next/link";
      import { usePathname } from "next/navigation";
      
      const links = [
        { href: "/", label: "Home" },
        { href: "/teas", label: "Teas" },
        { href: "/cart", label: "Cart" },
      ];
      
      export default function NavLinks() {
        const pathname = usePathname();
        return (
          <ul>
            {links.map((link) => {
              const active = link.href === "/" ? pathname === "/" : pathname === link.href || pathname.startsWith(`${link.href}/`);
              return (
                <li key={link.href}>
                  <Link href={link.href} aria-current={active ? "page" : undefined}>
                    {link.label}
                  </Link>
                </li>
              );
            })}
          </ul>
        );
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFollow the history

        A visitor goes from /cart to /checkout, presses Place order, then presses the browser's Back button. Which page shows?

        Starter codeTSX
        "use client";
        
        import { useRouter } from "next/navigation";
        
        export default function CheckoutButtons() {
          const router = useRouter();
          return (
            <p>
              <button onClick={() => router.back()}>Back to cart</button>{" "}
              <button onClick={() => router.replace("/thanks")}>Place order</button>
            </p>
          );
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeWhat does a refresh keep?

          A visitor types Leave it at the door into the note field, then presses Add a mint tea. What does the note field hold afterwards?

          Starter codeTSX
          "use client";
          
          import { useState } from "react";
          import { useRouter } from "next/navigation";
          
          export default function CartButtons() {
            const router = useRouter();
            const [note, setNote] = useState("");
          
            function addMint() {
              document.cookie = "cart=mint; path=/";
              router.refresh();
            }
          
            return (
              <div>
                <label>
                  Note for the driver <input value={note} onChange={(event) => setNote(event.target.value)} />
                </label>
                <p>
                  <button onClick={addMint}>Add a mint tea</button> <button onClick={() => router.push("/checkout")}>Checkout</button>
                </p>
              </div>
            );
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengePredict the scroll

            The visitor scrolled to the bottom of this page, 1092 px down, and clicked Green tea. How far down is the window after the click?

            Starter codeTSX
            import Link from "next/link";
            
            export default function TeasPage() {
              return (
                <main>
                  <h1>Our teas</h1>
                  <div style={{ height: 1600 }} />
                  <Link href="/teas/green" scroll={false}>Green tea</Link>
                </main>
              );
            }
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask what the router already had before the click, and what it had to ask the server for.

              The Linking and navigating quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does <Link href="/teas">Teas</Link> put in the page's HTML?

                Choose an answer to see the explanation.

              2. Question 2 of 7With partial prefetching on, as in new projects, what does a Link to /teas prefetch when it comes on the screen?

                Choose an answer to see the explanation.

              3. Question 3 of 7The Gift boxes link was prefetched while the visitor scrolled. The page is prerendered as static content. How many requests does the click make?

                Choose an answer to see the explanation.

              4. Question 4 of 7This is the checkout's buttons. The visitor came from /cart to /checkout, then pressed Place order. Then they press the browser's Back button. Which page shows?

                Read the code, then predictTSX
                "use client";
                
                import { useRouter } from "next/navigation";
                
                export default function CheckoutButtons() {
                  const router = useRouter();
                  return (
                    <p>
                      <button onClick={() => router.back()}>Back to cart</button>{" "}
                      <button onClick={() => router.replace("/thanks")}>Place order</button>
                    </p>
                  );
                }
                

                Choose an answer to see the explanation.

              5. Question 5 of 7A visitor typed a note into the cart, then pressed Add a mint tea, which calls router.refresh(). What happens to the note?

                Choose an answer to see the explanation.

              6. Question 6 of 7When does useLinkStatus() return pending: true?

                Choose an answer to see the explanation.

              7. Question 7 of 7A visitor clicks a Link inside a page, and that page is replaced by the new one. Where is the keyboard focus after the navigation?

                Choose an answer to see the explanation.

              Key takeaways

              • Link renders an <a href>. Once the page's JavaScript runs, a click to a page under the same root layout becomes a client-side navigation: the address changes, the page part is swapped, and the layouts stay. A page under another root layout loads as a whole new page.
              • In production, a Link in the viewport prefetches the linked route's static shell. The click shows it, and the router asks only for the parts made for each request. prefetch={false} waits for the click.
              • Mark the current page's link from a Client Component with usePathname and aria-current="page".
              • useRouter navigates from code: push adds a history entry, replace swaps it, refresh re-runs the Server Components and keeps browser state, back goes back.
              • useLinkStatus shows a link is waiting; onNavigate can stop a navigation.
              • Next.js scrolls to the new page when its top is out of view (scroll={false} stops it), leaves focus where it was, and announces the new title to screen readers.

              Remember the one-liner.
              Link fetches the shell before the click, and only the rest after it.

              Read it in Next.js's own code

              At release v16.4.3, Link watches the viewport with one shared observer, which starts a prefetch when a link comes within 200 pixels of the screen (links.ts, line 125). The scroll rule is in layout-router.tsx, lines 234 to 243. The Under the hood stage reads the router.

              Coming next: Dynamic routes. It replaces the shop's one folder per tea with a single folder that matches every tea, and shows how a page reads which one was asked for.

              CompleteFrontend Clear concepts. Working examples.