Skip to content

Commit c94753c

Browse files
Docs: Update useLinkStatus API reference (vercel#78022)
Updating the docs after playing around with `useLinkStatus`: vercel/next-app-router-playground#184 Closes: https://linear.app/vercel/issue/DOC-4514/update-docs Main changes: - Update introduction - Simplify example - Add example for gracefully handling fast navigations (avoiding UI flash)
1 parent 5b243a2 commit c94753c

File tree

1 file changed

+90
-78
lines changed

1 file changed

+90
-78
lines changed

docs/01-app/05-api-reference/04-functions/use-link-status.mdx

Lines changed: 90 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
---
22
title: useLinkStatus
33
description: API Reference for the useLinkStatus hook.
4+
related:
5+
title: Next Steps
6+
description: Learn more about the features mentioned in this page by reading the API Reference.
7+
links:
8+
- app/api-reference/components/link
9+
- app/api-reference/file-conventions/loading
410
---
511

6-
`useLinkStatus` is a **Client Component** hook that lets you track the loading state of a `Link` component during navigation. It can be used to show loading indicators during page transitions, especially when [prefetching](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) is disabled, or the linked route does not have any loading states.
12+
The `useLinkStatus` hook lets you tracks the **pending** state of a `<Link>`. You can use it to show inline visual feedback to the user (like spinners or text glimmers) while a navigation to a new route completes.
13+
14+
`useLinkStatus` is useful when:
15+
16+
- [Prefetching](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) is disabled or in progress meaning navigation is blocked.
17+
- The destination route is dynamic **and** doesn't include a [`loading.js`](/docs/app/api-reference/file-conventions/loading) file that would allow an instant navigation.
718

819
```tsx filename="app/loading-indicator.tsx" switcher
920
'use client'
@@ -12,7 +23,9 @@ import { useLinkStatus } from 'next/link'
1223

1324
export default function LoadingIndicator() {
1425
const { pending } = useLinkStatus()
15-
return pending ? <span>⌛</span> : null
26+
return pending ? (
27+
<div role="status" aria-label="Loading" className="spinner" />
28+
) : null
1629
}
1730
```
1831

@@ -23,7 +36,9 @@ import { useLinkStatus } from 'next/link'
2336

2437
export default function LoadingIndicator() {
2538
const { pending } = useLinkStatus()
26-
return pending ? <span></span> : null
39+
return pending ? (
40+
<div role="status" aria-label="Loading" className="spinner" />
41+
) : null
2742
}
2843
```
2944

@@ -81,13 +96,11 @@ const { pending } = useLinkStatus()
8196
| -------- | ------- | -------------------------------------------- |
8297
| pending | boolean | `true` before history updates, `false` after |
8398

84-
## Examples
85-
86-
### Improving the user experience when navigating with new query parameters
99+
## Example
87100

88-
In this example, navigating between categories updates the query string (e.g. ?category=books). However, the page may appear unresponsive because the `<PageSkeleton />` fallback won't replace the existing content (see [preventing unwanted loading indicators](https://react.dev/reference/react/useTransition#preventing-unwanted-loading-indicators)).
101+
### Inline loading indicator
89102

90-
You can use the `useLinkStatus` hook to render a lightweight loading indicator next to the active link and provide immediate feedback to the user while the data is being fetched.
103+
It's helpful to add visual feedback that navigation is happening in case the user clicks a link before prefetching is complete.
91104

92105
```tsx filename="app/components/loading-indicator.tsx" switcher
93106
'use client'
@@ -96,7 +109,9 @@ import { useLinkStatus } from 'next/link'
96109

97110
export default function LoadingIndicator() {
98111
const { pending } = useLinkStatus()
99-
return pending ? <span>⌛</span> : null
112+
return pending ? (
113+
<div role="status" aria-label="Loading" className="spinner" />
114+
) : null
100115
}
101116
```
102117

@@ -107,105 +122,102 @@ import { useLinkStatus } from 'next/link'
107122

108123
export default function LoadingIndicator() {
109124
const { pending } = useLinkStatus()
110-
return pending ? <span></span> : null
125+
return pending ? (
126+
<div role="status" aria-label="Loading" className="spinner" />
127+
) : null
111128
}
112129
```
113130

114-
```tsx filename="app/page.tsx" switcher
115-
import { useSearchParams } from 'next/navigation'
131+
```tsx filename="app/shop/layout.tsx" switcher
116132
import Link from 'next/link'
117-
import { Suspense } from 'react'
118-
import LoadingIndicator from './loading-indicator'
133+
import LoadingIndicator from './components/loading-indicator'
134+
135+
const links = [
136+
{ href: '/shop/electronics', label: 'Electronics' },
137+
{ href: '/shop/clothing', label: 'Clothing' },
138+
{ href: '/shop/books', label: 'Books' },
139+
]
119140

120-
function MenuBar() {
141+
function Menubar() {
121142
return (
122143
<div>
123-
<Link href="?category=electronics">
124-
Electronics <LoadingIndicator />
125-
</Link>
126-
<Link href="?category=clothing">
127-
Clothing <LoadingIndicator />
128-
</Link>
129-
<Link href="?category=books">
130-
Books <LoadingIndicator />
131-
</Link>
144+
{links.map((link) => (
145+
<Link key={link.label} href={link.href}>
146+
{link.label} <LoadingIndicator />
147+
</Link>
148+
))}
132149
</div>
133150
)
134151
}
135152

136-
async function ProductList({ category }: { category: string }) {
137-
const products = await fetchProducts(category)
138-
153+
export default function Layout({ children }: { children: React.ReactNode }) {
139154
return (
140-
<ul>
141-
{products.map((product) => (
142-
<li key={product}>{product}</li>
143-
))}
144-
</ul>
155+
<div>
156+
<Menubar />
157+
{children}
158+
</div>
145159
)
146160
}
161+
```
162+
163+
```jsx filename="app/shop/layout.js" switcher
164+
import Link from 'next/link'
165+
import LoadingIndicator from './components/loading-indicator'
147166

148-
export default async function ProductCategories({
149-
searchParams,
150-
}: {
151-
searchParams: Promise<{
152-
category: string
153-
}>
154-
}) {
155-
const { category } = await searchParams
167+
const links = [
168+
{ href: '/shop/electronics', label: 'Electronics' },
169+
{ href: '/shop/clothing', label: 'Clothing' },
170+
{ href: '/shop/books', label: 'Books' },
171+
]
156172

173+
function Menubar() {
157174
return (
158-
<Suspense fallback={<PageSkeleton />}>
159-
<MenuBar />
160-
<ProductList category={category} />
161-
</Suspense>
175+
<div>
176+
{links.map((link) => (
177+
<Link key={link.label} href={link.href}>
178+
{link.label} <LoadingIndicator />
179+
</Link>
180+
))}
181+
</div>
162182
)
163183
}
164-
```
165184

166-
```jsx filename="app/page.js" switcher
167-
import { useSearchParams } from 'next/navigation'
168-
import Link from 'next/link'
169-
import { Suspense } from 'react'
170-
import LoadingIndicator from './loading-indicator'
171-
172-
function MenuBar() {
185+
export default function Layout({ children }) {
173186
return (
174187
<div>
175-
<Link href="?category=electronics">
176-
Electronics <LoadingIndicator />
177-
</Link>
178-
<Link href="?category=clothing">
179-
Clothing <LoadingIndicator />
180-
</Link>
181-
<Link href="?category=books">
182-
Books <LoadingIndicator />
183-
</Link>
188+
<Menubar />
189+
{children}
184190
</div>
185191
)
186192
}
193+
```
187194

188-
async function ProductList({ category }) {
189-
const products = await fetchProducts(category)
195+
## Gracefully handling fast navigation
190196

191-
return (
192-
<ul>
193-
{products.map((product) => (
194-
<li key={product}>{product}</li>
195-
))}
196-
</ul>
197-
)
197+
If the navigation to a new route is fast, users may see an unecessary flash of the loading indicator. One way to improve the user experience and only show the loading indicator when the navigation takes time to complete is to add an initial animation delay (e.g. 100ms) and start the animation as invisible (e.g. `opacity: 0`).
198+
199+
```css filename="app/styles/global.css"
200+
.spinner {
201+
/* ... */
202+
opacity: 0;
203+
animation:
204+
fadeIn 500ms 100ms forwards,
205+
rotate 1s linear infinite;
198206
}
199207

200-
export default async function ProductCategories({ searchParams }) {
201-
const { category } = await searchParams
208+
@keyframes fadeIn {
209+
from {
210+
opacity: 0;
211+
}
212+
to {
213+
opacity: 1;
214+
}
215+
}
202216

203-
return (
204-
<Suspense fallback={<PageSkeleton />}>
205-
<MenuBar />
206-
<ProductList category={category} />
207-
</Suspense>
208-
)
217+
@keyframes rotate {
218+
to {
219+
transform: rotate(360deg);
220+
}
209221
}
210222
```
211223

0 commit comments

Comments
 (0)