Skip to content

Commit a1a4a4f

Browse files
committed
Add Next App Router example with onNavigate
1 parent b2d425b commit a1a4a4f

14 files changed

Lines changed: 301 additions & 0 deletions

File tree

.github/copilot-instructions.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ npm run test:umdprod # Test UMD prod build
125125
- Custom hooks follow `use*` naming convention
126126
- Export everything through main `index.tsx`
127127

128+
### Code Comment Style
129+
130+
- Use `//` line comments, not `/* */` or `/** */` block comments
131+
- Wrap comment lines to fit within Prettier's line width (80 characters by default in this project)
132+
- Keep comments concise and informative — explain _why_, not _what_
133+
128134
### Language & Documentation Standards
129135

130136
- **Use New Zealand English** at all times (e.g., "colour", "behaviour", "centre", "organisation")

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ render(<Enhanced isAnimating />, document.getElementById('root'))
9797
- HOC: [Source](https://github.com/tanem/react-nprogress/tree/master/examples/hoc) | [Sandbox](https://codesandbox.io/s/github/tanem/react-nprogress/tree/master/examples/hoc)
9898
- Material UI: [Source](https://github.com/tanem/react-nprogress/tree/master/examples/material-ui) | [Sandbox](https://codesandbox.io/s/github/tanem/react-nprogress/tree/master/examples/material-ui)
9999
- Multiple Instances: [Source](https://github.com/tanem/react-nprogress/tree/master/examples/multiple-instances) | [Sandbox](https://codesandbox.io/s/github/tanem/react-nprogress/tree/master/examples/multiple-instances)
100+
- Next App Router: [Source](https://github.com/tanem/react-nprogress/tree/master/examples/next-app-router) | [Sandbox](https://codesandbox.io/s/github/tanem/react-nprogress/tree/master/examples/next-app-router)
100101
- Next Pages Router: [Source](https://github.com/tanem/react-nprogress/tree/master/examples/next-pages-router) | [Sandbox](https://codesandbox.io/s/github/tanem/react-nprogress/tree/master/examples/next-pages-router)
101102
- Original Design: [Source](https://github.com/tanem/react-nprogress/tree/master/examples/original-design) | [Sandbox](https://codesandbox.io/s/github/tanem/react-nprogress/tree/master/examples/original-design)
102103
- Plain JS: [Source](https://github.com/tanem/react-nprogress/tree/master/examples/plain-js) | [Sandbox](https://codesandbox.io/s/github/tanem/react-nprogress/tree/master/examples/plain-js)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.next/
2+
/node_modules

examples/next-app-router/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# ReactNProgress Next App Router Example
2+
3+
Demonstrates `@tanem/react-nprogress` with the Next.js App Router. Since the App
4+
Router does not expose `router.events`, this example uses:
5+
6+
- **`onNavigate`** (Next.js 15.3+) on a `<ProgressLink>` wrapper to detect
7+
navigation start.
8+
- **`usePathname()` / `useSearchParams()`** to detect navigation completion, as
9+
recommended by the [Next.js
10+
docs](https://nextjs.org/docs/app/api-reference/functions/use-router#router-events).
11+
12+
To run it:
13+
14+
```
15+
$ npm i && npm run dev
16+
```
17+
18+
Then open [http://localhost:3000](http://localhost:3000) to view it in the
19+
browser.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default async function AboutPage() {
2+
await new Promise((resolve) => {
3+
setTimeout(resolve, 500)
4+
})
5+
6+
return <p>This is about Next.js!</p>
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default async function ForeverPage() {
2+
await new Promise((resolve) => {
3+
setTimeout(resolve, 3000)
4+
})
5+
6+
return <p>This page was rendered for a while!</p>
7+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import NavigationProgress from '../components/NavigationProgress'
2+
import ProgressLink from '../components/ProgressLink'
3+
4+
export default function RootLayout({
5+
children,
6+
}: {
7+
children: React.ReactNode
8+
}) {
9+
return (
10+
<html lang="en">
11+
<body>
12+
<NavigationProgress>
13+
<nav>
14+
<ProgressLink href="/" style={{ marginRight: 10 }}>
15+
Home
16+
</ProgressLink>
17+
<ProgressLink href="/about" style={{ marginRight: 10 }}>
18+
About
19+
</ProgressLink>
20+
<ProgressLink href="/forever" style={{ marginRight: 10 }}>
21+
Forever
22+
</ProgressLink>
23+
<a href="/non-existing">Non Existing Page</a>
24+
</nav>
25+
{children}
26+
</NavigationProgress>
27+
</body>
28+
</html>
29+
)
30+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function HomePage() {
2+
return <p>Hello Next.js!</p>
3+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use client'
2+
3+
import { useNProgress } from '@tanem/react-nprogress'
4+
5+
const Loading: React.FC<{ isRouteChanging: boolean }> = ({
6+
isRouteChanging,
7+
}) => {
8+
const { animationDuration, isFinished, progress } = useNProgress({
9+
isAnimating: isRouteChanging,
10+
})
11+
12+
return (
13+
<div
14+
style={{
15+
opacity: isFinished ? 0 : 1,
16+
pointerEvents: 'none',
17+
transition: `opacity ${animationDuration}ms linear`,
18+
}}
19+
>
20+
<div
21+
style={{
22+
background: '#29d',
23+
height: '2px',
24+
left: 0,
25+
marginLeft: `${(-1 + progress) * 100}%`,
26+
position: 'fixed',
27+
top: 0,
28+
transition: `margin-left ${animationDuration}ms linear`,
29+
width: '100%',
30+
zIndex: 1031,
31+
}}
32+
>
33+
<div
34+
style={{
35+
boxShadow: '0 0 10px #29d, 0 0 5px #29d',
36+
display: 'block',
37+
height: '100%',
38+
opacity: 1,
39+
position: 'absolute',
40+
right: 0,
41+
transform: 'rotate(3deg) translate(0px, -4px)',
42+
width: '100px',
43+
}}
44+
/>
45+
</div>
46+
</div>
47+
)
48+
}
49+
50+
export default Loading
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use client'
2+
3+
import { usePathname, useSearchParams } from 'next/navigation'
4+
import {
5+
createContext,
6+
Suspense,
7+
useCallback,
8+
useContext,
9+
useEffect,
10+
useRef,
11+
useState,
12+
} from 'react'
13+
14+
import Loading from './Loading'
15+
16+
type NavigationProgressContextType = {
17+
start(): void
18+
}
19+
20+
const NavigationProgressContext =
21+
createContext<NavigationProgressContextType | null>(null)
22+
23+
export function useNavigationProgress() {
24+
const context = useContext(NavigationProgressContext)
25+
if (!context) {
26+
throw new Error(
27+
'useNavigationProgress must be used within <NavigationProgress>',
28+
)
29+
}
30+
return context
31+
}
32+
33+
// Watches pathname/searchParams changes to detect when
34+
// navigation has completed. Wrapped in Suspense because
35+
// useSearchParams() requires a Suspense boundary.
36+
function NavigationComplete({ onComplete }: { onComplete: () => void }) {
37+
const pathname = usePathname()
38+
const searchParams = useSearchParams()
39+
const currentUrl = useRef(pathname + searchParams.toString())
40+
41+
useEffect(() => {
42+
const newUrl = pathname + searchParams.toString()
43+
if (newUrl !== currentUrl.current) {
44+
currentUrl.current = newUrl
45+
onComplete()
46+
}
47+
}, [pathname, searchParams, onComplete])
48+
49+
return null
50+
}
51+
52+
// Provides navigation progress state to the component
53+
// tree. Navigation start is signalled via the onNavigate
54+
// prop on a <ProgressLink>, and completion is detected by
55+
// watching usePathname()/useSearchParams().
56+
export default function NavigationProgress({
57+
children,
58+
}: {
59+
children: React.ReactNode
60+
}) {
61+
const [isRouteChanging, setIsRouteChanging] = useState(false)
62+
const [loadingKey, setLoadingKey] = useState(0)
63+
64+
const contextValue = useRef<NavigationProgressContextType>({
65+
start: () => {
66+
setIsRouteChanging(true)
67+
setLoadingKey((prev) => prev ^ 1)
68+
},
69+
}).current
70+
71+
const handleComplete = useCallback(() => setIsRouteChanging(false), [])
72+
73+
return (
74+
<NavigationProgressContext.Provider value={contextValue}>
75+
<Loading isRouteChanging={isRouteChanging} key={loadingKey} />
76+
<Suspense>
77+
<NavigationComplete onComplete={handleComplete} />
78+
</Suspense>
79+
{children}
80+
</NavigationProgressContext.Provider>
81+
)
82+
}

0 commit comments

Comments
 (0)