Skip to content

lightweight, zero-dependency library providing accurate navigation tracking, fixed document.referrer value, and complete history support for React frameworks. Fully compatible with Next.js, Remix, TanStack Query, and React Router.

License

Notifications You must be signed in to change notification settings

hsuanyi-chou/react-page-tracker

Repository files navigation

React Page Tracker

react-page-tracker Logo

react-page-tracker is a lightweight, zero-dependency library providing accurate navigation tracking, fixed document.referrer value, and complete history support for React frameworks. Fully compatible with Next.js, Remix, TanStack Query, and React Router.

👉 Demo

📖 Documentation

🚀 An awesome use case: Magic Back Button in shadcn/ui expansions

Features

  • 📝 Identifies whether the user navigated to the page via browser back/forward buttons or by clicking a link.
  • 🧩 Works with History.go(), history.forward(), history.back(), and history.pushState().
  • 🐞 Fixes incorrect document.referrer after navigation, providing the correct referrer for tracking purposes.
  • 💡 Accurately determines whether the current page is the first or last page.
  • 🧭 Offers a complete history browsing record.
  • 🚀 Supports Next.js, Remix, TanStack Query, and React Router.
  • ⚡️ zero deps.
  • ⭐️ typed-safe.

Usage

type PageTrackerState = {
  /** current page index */
  pageIndex: number;
  /** correct `document.referrer` */
  referrer: string;
  /** whether the current page is the first page */
  isFirstPage: boolean;
  /** whether the current page is the last page */
  isLastPage: boolean;
  /** whether the user navigated to the page via browser back/forward buttons or by clicking a link */
  pageEvent?: PageEvent; // 'back' | 'forward' | 'push' | undefined.  undefined for first visit.
  /** history browsing record */
  pageHistory: string[]; // ['/', '/products', '/pdocuts/1', '/products/2', '/about', ...]
  /**
   * total page history length.
   * When user press `back` button, the `pageHistory`'s end link will become the current link.
   * You may need this total length to handle `history.go(N)` to forward N page.
   */  
  pageHistoryLength: number;
}

Simply to get the values you need in any component.

  const { pageIndex, referrer, isFirstPage, isLastPage, pageEvent, pageHistory } = usePageTrackerStore((state) => state);

Installation

npm install react-page-tracker
▲ Next.js

layout.tsx

+ import { PageTracker } from 'react-page-tracker';
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) {
  return (
    <html lang="en">
    <body>
+      <PageTracker /> // next 14, there's an auto detection. A better way is to follow your environment. e,g. `enableStrictModeHandler={process.env.NODE_ENV === 'local'}`
+      <PageTracker enableStrictModeHandler={false} /> // next 15+. always be false
        {children}
    </body>
    </html>
  );
}

If you are using nextjs 15, you have to set enableStrictModeHandler to false

In nextjs 14, strict mode will execute twice when push a page (e,g. click an Anchor link) which may cause pageHistory values error and hard to handle in development mode.

But in remix, tanstack/query, react-router, it will not execute twice. works fine.

There's an automatic detection for nextjs strict mode, so you don't need to set this prop.

But I can't detect 14 or 15, so you have to set this prop to false in next 15.

PS: DO NOT set enableStrictModeHandler to true in production, it may cause issues.

You are all set up! Now you can access the page tracking information in any component.

component.tsx

import { usePageTrackerStore } from 'react-page-tracker';

export const PageTrackerValue = () => {
  // get all state
  const state = usePageTrackerStore((state) => state);
  // get the specific state you'd like to use
  const pickState = usePageTrackerStore((state) => ({
    isFirstPage: state.isFirstPage,
    referrer: state.referrer,
    pageHistory: state.pageHistory,
  }));

  return (
    <div className="flex">
      <div className="flex flex-col gap-3">
        <pre className="rounded-md bg-gray-100 px-2 py-0.5 font-bold">
          state: {JSON.stringify(state)}
        </pre>
        <pre className="rounded-md bg-gray-100 px-2 py-0.5 font-bold">
          pickState: {JSON.stringify(pickState)}
        </pre>
      </div>
    </div>
  );
};

Next.js template

🆁 Remix

force Vite to build to CommonJS

vite.config.ts

export default defineConfig({
+  ssr: {
+    noExternal: ['react-page-tracker'], // force Vite to build to CommonJS
+  },
  plugins: [
    remix({
      future: {
        v3_fetcherPersist: true,
        v3_relativeSplatPath: true,
        v3_throwAbortReason: true,
        v3_singleFetch: true,
        v3_lazyRouteDiscovery: true,
      },
    }),
    tsconfigPaths(),
  ],
});

root.tsx

import { Links, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
import type { LinksFunction } from '@remix-run/node';

import './tailwind.css';
+ import { PageTracker } from 'react-page-tracker';

export const links: LinksFunction = () => [
  { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
  {
    rel: 'preconnect',
    href: 'https://fonts.gstatic.com',
    crossOrigin: 'anonymous',
  },
  {
    rel: 'stylesheet',
    href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
  },
];

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
+       <PageTracker />
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}

You are all set up! Now you can access the page tracking information in any component.

component.tsx

import { usePageTrackerStore } from 'react-page-tracker';

export const PageTrackerValue = () => {
  // get all state
  const state = usePageTrackerStore((state) => state);
  // get the specific state you'd like to use
  const pickState = usePageTrackerStore((state) => ({
    isFirstPage: state.isFirstPage,
    referrer: state.referrer,
    pageHistory: state.pageHistory,
  }));

  return (
    <div className="flex">
      <div className="flex flex-col gap-3">
        <pre className="rounded-md bg-gray-100 px-2 py-0.5 font-bold">
          state: {JSON.stringify(state)}
        </pre>
        <pre className="rounded-md bg-gray-100 px-2 py-0.5 font-bold">
          pickState: {JSON.stringify(pickState)}
        </pre>
      </div>
    </div>
  );
};

Remix template

Contributing

Be welcome to contribute! Here's how you can contribute:

  • Open an issue if you believe you've encountered a bug.
  • Make a pull request to add new features/make quality-of-life improvements/fix bugs.

Built by Hsuan Yi, Chou

BuyMeACoffee

License

Licensed under the MIT License.

About

lightweight, zero-dependency library providing accurate navigation tracking, fixed document.referrer value, and complete history support for React frameworks. Fully compatible with Next.js, Remix, TanStack Query, and React Router.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

Packages

No packages published