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.
- 01Link 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.
- 02Navigate from codeMark the current page's link with usePathname, and use push, replace, refresh and back from useRouter.
- 03Handle 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.
app/layout.tsximport 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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
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
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:
- Open /. One
documentrequest, for the HTML page. Then, before anyone clicked, fetches for/teaswith the headernext-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 theChecking stock…fallback. - Click Teas. The address is
/teas, and the page showsOur teasandChecking stock…. There's one fetch, without the prefetch header: the router asking for the part it didn't have, the stock. Its answer istext/x-component, an RSC payload: the data React uses to draw what the Server Components rendered. (How Next.js works showed one.) - Wait for the stock.
In stock: 12 boxestakes the place ofChecking stock…, from the same request. - Click Home. No requests at all: the home page was prefetched when the page opened, and nothing on it is made for each request.
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.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
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.
The Link component
Link is a component that comes with Next.js. You import it with import Link from "next/link", as on line 1 of the layout, and give it an href. Anything an <a> takes, like className or aria-current, passes through to the <a> it renders. It has a few props of its own. Each row is one prop: what it does, and what happens when you leave it out.
| Prop | What it does | When you leave it out |
|---|---|---|
href | Where the link goes: a path like /teas, or an object like { pathname: "/teas", query: { sort: "price" } }. | Required. |
prefetch | How much to load before the click. Leave it out for the default; false never prefetches, in the viewport or on hover. | The default prefetches the static shell when the link is on the screen, and again on hover if that prefetch has expired. |
replace | Replace the current entry in the browser's history instead of adding one. | false: a click adds an entry, and Back returns to this page. |
scroll | false keeps the window where it is after the click. | true: Next.js scrolls to the new page when its top is out of view. |
onNavigate | A function called when the click starts a client-side navigation. It can stop it with preventDefault(). | None. |
Use Link for every page of your own app. For another website, a plain <a> is right: there's nothing of yours to prefetch or keep.
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).
app/layout.tsximport 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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
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
GET/200a whole pageGET/200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /_tree
- next-url
- /
GET/teas200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /_tree
- next-url
- /
GET/teas200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /teas/__PAGE__
- next-url
- /
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.
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:
app/page.tsximport 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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
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
GET/200a whole pageGET/200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /_tree
- next-url
- /
GET/teas200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /_tree
- next-url
- /
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.
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 buildandnext start; innext 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.
Showing which page the visitor is on
A menu usually marks the link of the current page, so visitors know where they are. The layout can't know the path, as the previous lesson showed, so the menu moves into a Client Component that calls usePathname:
app/nav-links.tsx"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> );}- Teas
- Cart
Fresh tea, sent to your door.
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
Line 13 reads the path. Line 17 decides whether each link is the active one: / only on the home page, and any other link on its own path or a path below it. The / after link.href matters: without it, /teas would also match an address like /teaspoons. Line 20 sets aria-current="page" on the active link. It tells screen readers which link is the current page, and your CSS can style it with [aria-current="page"].
Pick /teas/green in the Browser tab: Teas is bold, because the recorded page marked it, and the Network tab shows aria-current="page" in the HTML. The server already rendered the right link, since usePathname works while Next.js renders a Client Component on the server too. The Steps tab shows it moving in Chrome: Home after step 1, Teas after the click on Teas, and still Teas on /teas/green.
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 tohref, adding an entry to the browser's history, like aLinkclick.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.
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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
In your cart: Mint tea
- Focus
- the button “Add a mint tea”
- Scrolled
- 0 px down
Requests the page made during this step (3), without the files it loaded
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
GET/200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /__PAGE__
- next-url
- /cart
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.
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.
app/layout.tsximport 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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
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
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
- /
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.
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:
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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
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.
app/teas/page.tsximport 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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
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
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.
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.
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.
| Write | When it fits | What 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:
- The shop's menu: Home, Teas and Cart.
- A list of teas, each going to its own page.
- A Checkout button that goes to
/checkoutwhen 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.
Sort each part of the tea shop: is it a Link, or code that calls useRouter?
Common mix-ups
- "
Linkdoesn't render a real link." No. It renders an<a href>, as the recorded HTML shows. - "A
Linkprefetches 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 withnext buildandnext 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.
What does line 10's <Link href="/teas">Teas</Link> become in the HTML that the server sends?
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>
);
}
An <a>: <a href="/teas">Teas</a>. The recorded HTML of / has it, so the link works before any JavaScript runs.
With this layout, when does the router first ask the server for /teas?
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>
);
}
When the visitor clicks. In the recording, opening / made no request for /teas; the click on Teas made the first one.
On /teas/green, which link does this menu mark with aria-current="page"?
"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>
);
}
Teas. /teas/green starts with /teas/, so the Teas link is active; Home is active only on /. The recorded HTML of /teas/green has aria-current="page" on Teas.
A visitor goes from /cart to /checkout, presses Place order, then presses the browser's Back button. Which page shows?
"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>
);
}
/cart. The history was /cart, then /checkout; Place order replaced /checkout with /thanks. Back goes to the entry before: the cart. The recording's last step is on /cart.
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?
"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>
);
}
Leave it at the door. note is state in a Client Component, and router.refresh() keeps it while the cart's Server Component runs again. The recording shows the note and In your cart: Mint tea together.
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?
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>
);
}
Where it was: 1092 px down. scroll={false} tells Next.js not to scroll after the navigation. With a plain Link, the recording shows 0 px.
Check your understanding
For each question, ask what the router already had before the click, and what it had to ask the server for.
Question 1 of 7What does
<Link href="/teas">Teas</Link>put in the page's HTML?Choose an answer to see the explanation.
Question 2 of 7With partial prefetching on, as in new projects, what does a
Linkto/teasprefetch when it comes on the screen?Choose an answer to see the explanation.
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.
Question 4 of 7This is the checkout's buttons. The visitor came from
/cartto/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.
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.
Question 6 of 7When does
useLinkStatus()returnpending: true?Choose an answer to see the explanation.
Question 7 of 7A visitor clicks a
Linkinside 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
Linkrenders 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
Linkin 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
usePathnameandaria-current="page". useRouternavigates from code:pushadds a history entry,replaceswaps it,refreshre-runs the Server Components and keeps browser state,backgoes back.useLinkStatusshows a link is waiting;onNavigatecan 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.
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.