Skip to content

Commit 4643bcd

Browse files
gaojudeleerobdelbaoliveira
authored
doc: onNavigate (#77647)
<!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Adding or Updating Examples - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # --> --------- Co-authored-by: Lee Robinson <[email protected]> Co-authored-by: Delba de Oliveira <[email protected]>
1 parent 8160f57 commit 4643bcd

File tree

1 file changed

+347
-16
lines changed
  • docs/01-app/04-api-reference/02-components

1 file changed

+347
-16
lines changed

Diff for: docs/01-app/04-api-reference/02-components/link.mdx

+347-16
Original file line numberDiff line numberDiff line change
@@ -55,27 +55,29 @@ The following props can be passed to the `<Link>` component:
5555

5656
<PagesOnly>
5757

58-
| Prop | Example | Type | Required |
59-
| ----------------------------------- | ----------------------- | ----------------- | -------- |
60-
| [`href`](#href-required) | `href="/dashboard"` | String or Object | Yes |
61-
| [`replace`](#replace) | `replace={false}` | Boolean | - |
62-
| [`scroll`](#scroll) | `scroll={false}` | Boolean | - |
63-
| [`prefetch`](#prefetch) | `prefetch={false}` | Boolean | - |
64-
| [`legacyBehavior`](#legacybehavior) | `legacyBehavior={true}` | Boolean | - |
65-
| [`passHref`](#passhref) | `passHref={true}` | Boolean | - |
66-
| [`shallow`](#shallow) | `shallow={false}` | Boolean | - |
67-
| [`locale`](#locale) | `locale="fr"` | String or Boolean | - |
58+
| Prop | Example | Type | Required |
59+
| ----------------------------------- | ------------------------ | ----------------- | -------- |
60+
| [`href`](#href-required) | `href="/dashboard"` | String or Object | Yes |
61+
| [`replace`](#replace) | `replace={false}` | Boolean | - |
62+
| [`scroll`](#scroll) | `scroll={false}` | Boolean | - |
63+
| [`prefetch`](#prefetch) | `prefetch={false}` | Boolean | - |
64+
| [`legacyBehavior`](#legacybehavior) | `legacyBehavior={true}` | Boolean | - |
65+
| [`passHref`](#passhref) | `passHref={true}` | Boolean | - |
66+
| [`shallow`](#shallow) | `shallow={false}` | Boolean | - |
67+
| [`locale`](#locale) | `locale="fr"` | String or Boolean | - |
68+
| [`onNavigate`](#onnavigate) | `onNavigate={(e) => {}}` | Function | - |
6869

6970
</PagesOnly>
7071

7172
<AppOnly>
7273

73-
| Prop | Example | Type | Required |
74-
| ------------------------ | ------------------- | ---------------- | -------- |
75-
| [`href`](#href-required) | `href="/dashboard"` | String or Object | Yes |
76-
| [`replace`](#replace) | `replace={false}` | Boolean | - |
77-
| [`scroll`](#scroll) | `scroll={false}` | Boolean | - |
78-
| [`prefetch`](#prefetch) | `prefetch={false}` | Boolean or null | - |
74+
| Prop | Example | Type | Required |
75+
| --------------------------- | ------------------------ | ---------------- | -------- |
76+
| [`href`](#href-required) | `href="/dashboard"` | String or Object | Yes |
77+
| [`replace`](#replace) | `replace={false}` | Boolean | - |
78+
| [`scroll`](#scroll) | `scroll={false}` | Boolean | - |
79+
| [`prefetch`](#prefetch) | `prefetch={false}` | Boolean or null | - |
80+
| [`onNavigate`](#onnavigate) | `onNavigate={(e) => {}}` | Function | - |
7981

8082
</AppOnly>
8183

@@ -450,6 +452,58 @@ export default function Home() {
450452

451453
</PagesOnly>
452454

455+
### `onNavigate`
456+
457+
An event handler called during client-side navigation. The handler receives an event object that includes a `preventDefault()` method, allowing you to cancel the navigation if needed.
458+
459+
```tsx filename="app/page.tsx" switcher
460+
import Link from 'next/link'
461+
462+
export default function Page() {
463+
return (
464+
<Link
465+
href="/dashboard"
466+
onNavigate={(e) => {
467+
// Only executes during SPA navigation
468+
console.log('Navigating...')
469+
470+
// Optionally prevent navigation
471+
// e.preventDefault()
472+
}}
473+
>
474+
Dashboard
475+
</Link>
476+
)
477+
}
478+
```
479+
480+
```jsx filename="app/page.js" switcher
481+
import Link from 'next/link'
482+
483+
export default function Page() {
484+
return (
485+
<Link
486+
href="/dashboard"
487+
onNavigate={(e) => {
488+
// Only executes during SPA navigation
489+
console.log('Navigating...')
490+
491+
// Optionally prevent navigation
492+
// e.preventDefault()
493+
}}
494+
>
495+
Dashboard
496+
</Link>
497+
)
498+
}
499+
```
500+
501+
> **Good to know**: While `onClick` and `onNavigate` may seem similar, they serve different purposes. `onClick` executes for all click events, while `onNavigate` only runs during client-side navigation. Some key differences:
502+
>
503+
> - When using modifier keys (`Ctrl`/`Cmd` + Click), `onClick` executes but `onNavigate` doesn't since Next.js prevents default navigation for new tabs.
504+
> - External URLs won't trigger `onNavigate` since it's only for client-side and same-origin navigations.
505+
> - Links with the `download` attribute will work with `onClick` but not `onNavigate` since the browser will treat the linked URL as a download.
506+
453507
## Examples
454508

455509
The following examples demonstrate how to use the `<Link>` component in different scenarios.
@@ -1178,10 +1232,287 @@ export default function Home() {
11781232
11791233
</PagesOnly>
11801234

1235+
### Blocking navigation
1236+
1237+
You can use the `onNavigate` prop to block navigation when certain conditions are met, such as when a form has unsaved changes. When you need to block navigation across multiple components in your app (like preventing navigation from any link while a form is being edited), React Context provides a clean way to share this blocking state. First, create a context to track the navigation blocking state:
1238+
1239+
```tsx filename="app/contexts/navigation-blocker.tsx" switcher
1240+
'use client'
1241+
1242+
import { createContext, useState, useContext } from 'react'
1243+
1244+
interface NavigationBlockerContextType {
1245+
isBlocked: boolean
1246+
setIsBlocked: (isBlocked: boolean) => void
1247+
}
1248+
1249+
export const NavigationBlockerContext =
1250+
createContext<NavigationBlockerContextType>({
1251+
isBlocked: false,
1252+
setIsBlocked: () => {},
1253+
})
1254+
1255+
export function NavigationBlockerProvider({
1256+
children,
1257+
}: {
1258+
children: React.ReactNode
1259+
}) {
1260+
const [isBlocked, setIsBlocked] = useState(false)
1261+
1262+
return (
1263+
<NavigationBlockerContext.Provider value={{ isBlocked, setIsBlocked }}>
1264+
{children}
1265+
</NavigationBlockerContext.Provider>
1266+
)
1267+
}
1268+
1269+
export function useNavigationBlocker() {
1270+
return useContext(NavigationBlockerContext)
1271+
}
1272+
```
1273+
1274+
```jsx filename="app/contexts/navigation-blocker.js" switcher
1275+
'use client'
1276+
1277+
import { createContext, useState, useContext } from 'react'
1278+
1279+
export const NavigationBlockerContext = createContext({
1280+
isBlocked: false,
1281+
setIsBlocked: () => {},
1282+
})
1283+
1284+
export function NavigationBlockerProvider({ children }) {
1285+
const [isBlocked, setIsBlocked] = useState(false)
1286+
1287+
return (
1288+
<NavigationBlockerContext.Provider value={{ isBlocked, setIsBlocked }}>
1289+
{children}
1290+
</NavigationBlockerContext.Provider>
1291+
)
1292+
}
1293+
1294+
export function useNavigationBlocker() {
1295+
return useContext(NavigationBlockerContext)
1296+
}
1297+
```
1298+
1299+
Create a form component that uses the context:
1300+
1301+
```tsx filename="app/components/form.tsx" switcher
1302+
'use client'
1303+
1304+
import { useNavigationBlocker } from '../contexts/navigation-blocker'
1305+
1306+
export default function Form() {
1307+
const { setIsBlocked } = useNavigationBlocker()
1308+
1309+
return (
1310+
<form
1311+
onSubmit={(e) => {
1312+
e.preventDefault()
1313+
setIsBlocked(false)
1314+
}}
1315+
onChange={() => setIsBlocked(true)}
1316+
>
1317+
<input type="text" name="name" />
1318+
<button type="submit">Save</button>
1319+
</form>
1320+
)
1321+
}
1322+
```
1323+
1324+
```jsx filename="app/components/form.js" switcher
1325+
'use client'
1326+
1327+
import { useNavigationBlocker } from '../contexts/navigation-blocker'
1328+
1329+
export default function Form() {
1330+
const { setIsBlocked } = useNavigationBlocker()
1331+
1332+
return (
1333+
<form
1334+
onSubmit={(e) => {
1335+
e.preventDefault()
1336+
setIsBlocked(false)
1337+
}}
1338+
onChange={() => setIsBlocked(true)}
1339+
>
1340+
<input type="text" name="name" />
1341+
<button type="submit">Save</button>
1342+
</form>
1343+
)
1344+
}
1345+
```
1346+
1347+
Create a custom Link component that blocks navigation:
1348+
1349+
```tsx filename="app/components/custom-link.tsx" switcher
1350+
'use client'
1351+
1352+
import Link from 'next/link'
1353+
import { useNavigationBlocker } from '../contexts/navigation-blocker'
1354+
1355+
interface CustomLinkProps extends React.ComponentProps<typeof Link> {
1356+
children: React.ReactNode
1357+
}
1358+
1359+
export function CustomLink({ children, ...props }: CustomLinkProps) {
1360+
const { isBlocked } = useNavigationBlocker()
1361+
1362+
return (
1363+
<Link
1364+
onNavigate={(e) => {
1365+
if (isBlocked) {
1366+
e.preventDefault()
1367+
if (!window.confirm('You have unsaved changes. Leave anyway?')) {
1368+
e.preventDefault()
1369+
}
1370+
}
1371+
}}
1372+
{...props}
1373+
>
1374+
{children}
1375+
</Link>
1376+
)
1377+
}
1378+
```
1379+
1380+
```jsx filename="app/components/custom-link.js" switcher
1381+
'use client'
1382+
1383+
import Link from 'next/link'
1384+
import { useNavigationBlocker } from '../contexts/navigation-blocker'
1385+
1386+
export function CustomLink({ children, ...props }) {
1387+
const { isBlocked } = useNavigationBlocker()
1388+
1389+
return (
1390+
<Link
1391+
onNavigate={(e) => {
1392+
if (isBlocked) {
1393+
e.preventDefault()
1394+
if (!window.confirm('You have unsaved changes. Leave anyway?')) {
1395+
e.preventDefault()
1396+
}
1397+
}
1398+
}}
1399+
{...props}
1400+
>
1401+
{children}
1402+
</Link>
1403+
)
1404+
}
1405+
```
1406+
1407+
Create a navigation component:
1408+
1409+
```tsx filename="app/components/nav.tsx" switcher
1410+
'use client'
1411+
1412+
import { CustomLink as Link } from './custom-link'
1413+
1414+
export default function Nav() {
1415+
return (
1416+
<nav>
1417+
<Link href="/">Home</Link>
1418+
<Link href="/about">About</Link>
1419+
</nav>
1420+
)
1421+
}
1422+
```
1423+
1424+
```jsx filename="app/components/nav.js" switcher
1425+
'use client'
1426+
1427+
import { CustomLink as Link } from './custom-link'
1428+
1429+
export default function Nav() {
1430+
return (
1431+
<nav>
1432+
<Link href="/">Home</Link>
1433+
<Link href="/about">About</Link>
1434+
</nav>
1435+
)
1436+
}
1437+
```
1438+
1439+
Finally, wrap your app with the `NavigationBlockerProvider` in the root layout and use the components in your page:
1440+
1441+
```tsx filename="app/layout.tsx" switcher
1442+
import { NavigationBlockerProvider } from './contexts/navigation-blocker'
1443+
1444+
export default function RootLayout({
1445+
children,
1446+
}: {
1447+
children: React.ReactNode
1448+
}) {
1449+
return (
1450+
<html lang="en">
1451+
<body>
1452+
<NavigationBlockerProvider>{children}</NavigationBlockerProvider>
1453+
</body>
1454+
</html>
1455+
)
1456+
}
1457+
```
1458+
1459+
```jsx filename="app/layout.js" switcher
1460+
import { NavigationBlockerProvider } from './contexts/navigation-blocker'
1461+
1462+
export default function RootLayout({ children }) {
1463+
return (
1464+
<html lang="en">
1465+
<body>
1466+
<NavigationBlockerProvider>{children}</NavigationBlockerProvider>
1467+
</body>
1468+
</html>
1469+
)
1470+
}
1471+
```
1472+
1473+
Then, use the `Nav` and `Form` components in your page:
1474+
1475+
```tsx filename="app/page.tsx" switcher
1476+
import Nav from './components/nav'
1477+
import Form from './components/form'
1478+
1479+
export default function Page() {
1480+
return (
1481+
<div>
1482+
<Nav />
1483+
<main>
1484+
<h1>Welcome to the Dashboard</h1>
1485+
<Form />
1486+
</main>
1487+
</div>
1488+
)
1489+
}
1490+
```
1491+
1492+
```jsx filename="app/page.js" switcher
1493+
import Nav from './components/nav'
1494+
import Form from './components/form'
1495+
1496+
export default function Page() {
1497+
return (
1498+
<div>
1499+
<Nav />
1500+
<main>
1501+
<h1>Welcome to the Dashboard</h1>
1502+
<Form />
1503+
</main>
1504+
</div>
1505+
)
1506+
}
1507+
```
1508+
1509+
When a user tries to navigate away using `CustomLink` while the form has unsaved changes, they'll be prompted to confirm before leaving.
1510+
11811511
## Version history
11821512

11831513
| Version | Changes |
11841514
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1515+
| `v15.3.0` | Add `onNavigate` API |
11851516
| `v13.0.0` | No longer requires a child `<a>` tag. A [codemod](/docs/app/building-your-application/upgrading/codemods#remove-a-tags-from-link-components) is provided to automatically update your codebase. |
11861517
| `v10.0.0` | `href` props pointing to a dynamic route are automatically resolved and no longer require an `as` prop. |
11871518
| `v8.0.0` | Improved prefetching performance. |

0 commit comments

Comments
 (0)