From a1a4a4f5366394dd31fae553958a6ed7b5bbc021 Mon Sep 17 00:00:00 2001 From: Tane Morgan <464864+tanem@users.noreply.github.com> Date: Mon, 16 Feb 2026 08:01:01 +1300 Subject: [PATCH] Add Next App Router example with onNavigate --- .github/copilot-instructions.md | 6 ++ README.md | 1 + examples/next-app-router/.gitignore | 2 + examples/next-app-router/README.md | 19 +++++ examples/next-app-router/app/about/page.tsx | 7 ++ examples/next-app-router/app/forever/page.tsx | 7 ++ examples/next-app-router/app/layout.tsx | 30 +++++++ examples/next-app-router/app/page.tsx | 3 + .../next-app-router/components/Loading.tsx | 50 +++++++++++ .../components/NavigationProgress.tsx | 82 +++++++++++++++++++ .../components/ProgressLink.tsx | 22 +++++ examples/next-app-router/next-env.d.ts | 5 ++ examples/next-app-router/package.json | 25 ++++++ examples/next-app-router/tsconfig.json | 42 ++++++++++ 14 files changed, 301 insertions(+) create mode 100644 examples/next-app-router/.gitignore create mode 100644 examples/next-app-router/README.md create mode 100644 examples/next-app-router/app/about/page.tsx create mode 100644 examples/next-app-router/app/forever/page.tsx create mode 100644 examples/next-app-router/app/layout.tsx create mode 100644 examples/next-app-router/app/page.tsx create mode 100644 examples/next-app-router/components/Loading.tsx create mode 100644 examples/next-app-router/components/NavigationProgress.tsx create mode 100644 examples/next-app-router/components/ProgressLink.tsx create mode 100644 examples/next-app-router/next-env.d.ts create mode 100644 examples/next-app-router/package.json create mode 100644 examples/next-app-router/tsconfig.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index efb5d1f2b..0f08e0206 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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") diff --git a/README.md b/README.md index 26978e4cd..051c72a6f 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ render(, 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) diff --git a/examples/next-app-router/.gitignore b/examples/next-app-router/.gitignore new file mode 100644 index 000000000..b61b35572 --- /dev/null +++ b/examples/next-app-router/.gitignore @@ -0,0 +1,2 @@ +.next/ +/node_modules diff --git a/examples/next-app-router/README.md b/examples/next-app-router/README.md new file mode 100644 index 000000000..645b97070 --- /dev/null +++ b/examples/next-app-router/README.md @@ -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 `` 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. diff --git a/examples/next-app-router/app/about/page.tsx b/examples/next-app-router/app/about/page.tsx new file mode 100644 index 000000000..f873e99b4 --- /dev/null +++ b/examples/next-app-router/app/about/page.tsx @@ -0,0 +1,7 @@ +export default async function AboutPage() { + await new Promise((resolve) => { + setTimeout(resolve, 500) + }) + + return

This is about Next.js!

+} diff --git a/examples/next-app-router/app/forever/page.tsx b/examples/next-app-router/app/forever/page.tsx new file mode 100644 index 000000000..7a0769afb --- /dev/null +++ b/examples/next-app-router/app/forever/page.tsx @@ -0,0 +1,7 @@ +export default async function ForeverPage() { + await new Promise((resolve) => { + setTimeout(resolve, 3000) + }) + + return

This page was rendered for a while!

+} diff --git a/examples/next-app-router/app/layout.tsx b/examples/next-app-router/app/layout.tsx new file mode 100644 index 000000000..86612ddde --- /dev/null +++ b/examples/next-app-router/app/layout.tsx @@ -0,0 +1,30 @@ +import NavigationProgress from '../components/NavigationProgress' +import ProgressLink from '../components/ProgressLink' + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + + + {children} + + + + ) +} diff --git a/examples/next-app-router/app/page.tsx b/examples/next-app-router/app/page.tsx new file mode 100644 index 000000000..fc4702faa --- /dev/null +++ b/examples/next-app-router/app/page.tsx @@ -0,0 +1,3 @@ +export default function HomePage() { + return

Hello Next.js!

+} diff --git a/examples/next-app-router/components/Loading.tsx b/examples/next-app-router/components/Loading.tsx new file mode 100644 index 000000000..3b73c2389 --- /dev/null +++ b/examples/next-app-router/components/Loading.tsx @@ -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 ( +
+
+
+
+
+ ) +} + +export default Loading diff --git a/examples/next-app-router/components/NavigationProgress.tsx b/examples/next-app-router/components/NavigationProgress.tsx new file mode 100644 index 000000000..9a74941d3 --- /dev/null +++ b/examples/next-app-router/components/NavigationProgress.tsx @@ -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(null) + +export function useNavigationProgress() { + const context = useContext(NavigationProgressContext) + if (!context) { + throw new Error( + 'useNavigationProgress must be used within ', + ) + } + 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 , 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({ + start: () => { + setIsRouteChanging(true) + setLoadingKey((prev) => prev ^ 1) + }, + }).current + + const handleComplete = useCallback(() => setIsRouteChanging(false), []) + + return ( + + + + + + {children} + + ) +} diff --git a/examples/next-app-router/components/ProgressLink.tsx b/examples/next-app-router/components/ProgressLink.tsx new file mode 100644 index 000000000..a10577ff6 --- /dev/null +++ b/examples/next-app-router/components/ProgressLink.tsx @@ -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 +// wherever you want the progress bar to appear during navigation. +export default function ProgressLink(props: React.ComponentProps) { + const { start } = useNavigationProgress() + + return ( + { + start() + props.onNavigate?.(e) + }} + /> + ) +} diff --git a/examples/next-app-router/next-env.d.ts b/examples/next-app-router/next-env.d.ts new file mode 100644 index 000000000..1b3be0840 --- /dev/null +++ b/examples/next-app-router/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/examples/next-app-router/package.json b/examples/next-app-router/package.json new file mode 100644 index 000000000..959e6c354 --- /dev/null +++ b/examples/next-app-router/package.json @@ -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" + } +} diff --git a/examples/next-app-router/tsconfig.json b/examples/next-app-router/tsconfig.json new file mode 100644 index 000000000..abe252433 --- /dev/null +++ b/examples/next-app-router/tsconfig.json @@ -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" + ] +}