Skip to content

Latest commit

 

History

History
254 lines (206 loc) · 7.87 KB

readme.md

File metadata and controls

254 lines (206 loc) · 7.87 KB

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.