Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ npm run test:umdprod # Test UMD prod build
- Custom hooks follow `use*` naming convention
- Export everything through main `index.tsx`

### Code Comment Style

- Use `//` line comments, not `/* */` or `/** */` block comments
- Wrap comment lines to fit within Prettier's line width (80 characters by default in this project)
- Keep comments concise and informative — explain _why_, not _what_

### Language & Documentation Standards

- **Use New Zealand English** at all times (e.g., "colour", "behaviour", "centre", "organisation")
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ render(<Enhanced isAnimating />, document.getElementById('root'))
- 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)
- 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)
- 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)
- 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)
- 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)
- 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)
- 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)
Expand Down
2 changes: 2 additions & 0 deletions examples/next-app-router/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.next/
/node_modules
19 changes: 19 additions & 0 deletions examples/next-app-router/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# ReactNProgress Next App Router Example

Demonstrates `@tanem/react-nprogress` with the Next.js App Router. Since the App
Router does not expose `router.events`, this example uses:

- **`onNavigate`** (Next.js 15.3+) on a `<ProgressLink>` wrapper to detect
navigation start.
- **`usePathname()` / `useSearchParams()`** to detect navigation completion, as
recommended by the [Next.js
docs](https://nextjs.org/docs/app/api-reference/functions/use-router#router-events).

To run it:

```
$ npm i && npm run dev
```

Then open [http://localhost:3000](http://localhost:3000) to view it in the
browser.
7 changes: 7 additions & 0 deletions examples/next-app-router/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default async function AboutPage() {
await new Promise((resolve) => {
setTimeout(resolve, 500)
})

return <p>This is about Next.js!</p>
}
7 changes: 7 additions & 0 deletions examples/next-app-router/app/forever/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default async function ForeverPage() {
await new Promise((resolve) => {
setTimeout(resolve, 3000)
})

return <p>This page was rendered for a while!</p>
}
30 changes: 30 additions & 0 deletions examples/next-app-router/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import NavigationProgress from '../components/NavigationProgress'
import ProgressLink from '../components/ProgressLink'

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<NavigationProgress>
<nav>
<ProgressLink href="/" style={{ marginRight: 10 }}>
Home
</ProgressLink>
<ProgressLink href="/about" style={{ marginRight: 10 }}>
About
</ProgressLink>
<ProgressLink href="/forever" style={{ marginRight: 10 }}>
Forever
</ProgressLink>
<a href="/non-existing">Non Existing Page</a>
</nav>
{children}
</NavigationProgress>
</body>
</html>
)
}
3 changes: 3 additions & 0 deletions examples/next-app-router/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function HomePage() {
return <p>Hello Next.js!</p>
}
50 changes: 50 additions & 0 deletions examples/next-app-router/components/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client'

import { useNProgress } from '@tanem/react-nprogress'

const Loading: React.FC<{ isRouteChanging: boolean }> = ({
isRouteChanging,
}) => {
const { animationDuration, isFinished, progress } = useNProgress({
isAnimating: isRouteChanging,
})

return (
<div
style={{
opacity: isFinished ? 0 : 1,
pointerEvents: 'none',
transition: `opacity ${animationDuration}ms linear`,
}}
>
<div
style={{
background: '#29d',
height: '2px',
left: 0,
marginLeft: `${(-1 + progress) * 100}%`,
position: 'fixed',
top: 0,
transition: `margin-left ${animationDuration}ms linear`,
width: '100%',
zIndex: 1031,
}}
>
<div
style={{
boxShadow: '0 0 10px #29d, 0 0 5px #29d',
display: 'block',
height: '100%',
opacity: 1,
position: 'absolute',
right: 0,
transform: 'rotate(3deg) translate(0px, -4px)',
width: '100px',
}}
/>
</div>
</div>
)
}

export default Loading
82 changes: 82 additions & 0 deletions examples/next-app-router/components/NavigationProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use client'

import { usePathname, useSearchParams } from 'next/navigation'
import {
createContext,
Suspense,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react'

import Loading from './Loading'

type NavigationProgressContextType = {
start(): void
}

const NavigationProgressContext =
createContext<NavigationProgressContextType | null>(null)

export function useNavigationProgress() {
const context = useContext(NavigationProgressContext)
if (!context) {
throw new Error(
'useNavigationProgress must be used within <NavigationProgress>',
)
}
return context
}

// Watches pathname/searchParams changes to detect when
// navigation has completed. Wrapped in Suspense because
// useSearchParams() requires a Suspense boundary.
function NavigationComplete({ onComplete }: { onComplete: () => void }) {
const pathname = usePathname()
const searchParams = useSearchParams()
const currentUrl = useRef(pathname + searchParams.toString())

useEffect(() => {
const newUrl = pathname + searchParams.toString()
if (newUrl !== currentUrl.current) {
currentUrl.current = newUrl
onComplete()
}
}, [pathname, searchParams, onComplete])

return null
}

// Provides navigation progress state to the component
// tree. Navigation start is signalled via the onNavigate
// prop on a <ProgressLink>, and completion is detected by
// watching usePathname()/useSearchParams().
export default function NavigationProgress({
children,
}: {
children: React.ReactNode
}) {
const [isRouteChanging, setIsRouteChanging] = useState(false)
const [loadingKey, setLoadingKey] = useState(0)

const contextValue = useRef<NavigationProgressContextType>({
start: () => {
setIsRouteChanging(true)
setLoadingKey((prev) => prev ^ 1)
},
}).current

const handleComplete = useCallback(() => setIsRouteChanging(false), [])

return (
<NavigationProgressContext.Provider value={contextValue}>
<Loading isRouteChanging={isRouteChanging} key={loadingKey} />
<Suspense>
<NavigationComplete onComplete={handleComplete} />
</Suspense>
{children}
</NavigationProgressContext.Provider>
)
}
22 changes: 22 additions & 0 deletions examples/next-app-router/components/ProgressLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client'

import Link from 'next/link'

import { useNavigationProgress } from './NavigationProgress'

// A thin wrapper around next/link that signals navigation start via the
// onNavigate callback introduced in Next.js 15.3. Use this in place of <Link>
// wherever you want the progress bar to appear during navigation.
export default function ProgressLink(props: React.ComponentProps<typeof Link>) {
const { start } = useNavigationProgress()

return (
<Link
{...props}
onNavigate={(e) => {
start()
props.onNavigate?.(e)
}}
/>
)
}
5 changes: 5 additions & 0 deletions examples/next-app-router/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
25 changes: 25 additions & 0 deletions examples/next-app-router/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "next-app-router",
"description": "ReactNProgress Next App Router Example",
"keywords": [
"@tanem/react-nprogress"
],
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@tanem/react-nprogress": "latest",
"next": "latest",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@types/node": "24.10.13",
"@types/react": "19.2.14",
"typescript": "5.9.3"
}
}
42 changes: 42 additions & 0 deletions examples/next-app-router/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}