npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@addilytics/react-router

v0.0.1

Published

React Router server middleware and bundled navigation tracking.

Readme

@addilytics/react-router

Server-side pageviews, SPA navigations, and custom events for React Router Framework Mode.

This adapter runs as root route middleware. It waits for React Router's final Response, including an error boundary response, then passes that exact object back unchanged. It records HTML document requests only. React Router .data requests and __manifest requests don't count as pageviews.

Server-only tracking remains the default. You can add a small browser entry that reports committed React Router navigations to a same-origin relay. The site key and ingest endpoint stay in server code. The helper is bundled by your app; Addilytics does not load a hosted or CDN script.

Manual setup

Create the middleware once and export it from app/root.tsx:

import { createAddilyticsMiddleware } from '@addilytics/react-router';

const analytics = createAddilyticsMiddleware({
	endpoint: 'https://addilytics.example',
	siteKey: process.env.ADDILYTICS_KEY!
});

export const middleware: Route.MiddlewareFunction[] = [analytics];

Order root middleware as authentication/session/context initialization, Addilytics, then routes and response-mutating middleware. Authentication must run first because relay requests finish inside the Addilytics middleware and do not reach later middleware. Addilytics must still wrap response mutators so it sees their final status and content type.

export const middleware: Route.MiddlewareFunction[] = [
	authAndSessionMiddleware,
	analytics,
	responseMiddleware
];

React Router 7 projects must enable future.v8_middleware in react-router.config.ts. React Router 8 doesn't need that flag.

The default status policy records 2xx and 404 HTML responses. Supply trackStatuses if you want a different policy. For example, this includes rendered error boundaries with a 500 status:

const analytics = createAddilyticsMiddleware({
	endpoint: 'https://addilytics.example',
	siteKey: process.env.ADDILYTICS_KEY!,
	trackStatuses: (status) => (status >= 200 && status < 300) || status === 404 || status === 500
});

SPA pageviews

Use hybrid mode to record the initial document request on the server and later client-side navigations in the browser. Set the same mode on both sides.

// app/addilytics.server.ts
import type { ReactRouterOptions } from '@addilytics/react-router';

export default {
	endpoint: 'https://addilytics.example',
	mode: 'hybrid',
	siteKey: process.env.ADDILYTICS_KEY!
} satisfies ReactRouterOptions;
// app/root.tsx
import { AddilyticsBrowser } from '@addilytics/react-router/browser';

export default function Root() {
	return (
		<html>
			<body>
				<AddilyticsBrowser mode="hybrid" />
				<Outlet />
				<Scripts />
			</body>
		</html>
	);
}

AddilyticsBrowser uses useLocation, so it runs after React Router commits a location. It renders nothing. Mount it once under the root router. Hybrid mode skips its first browser event because the server already recorded that document. client mode records the initial browser render and every later navigation, while mode: 'client' on the server disables automatic document pageviews.

React Router runs middleware only for matched routes. Add a resource route for the relay so the root middleware can intercept its POST before the fallback action runs:

// app/routes.ts
import { route } from '@react-router/dev/routes';

export default [
	// Your application routes...
	route('__addilytics', 'routes/addilytics.ts')
];
// app/routes/addilytics.ts
export function action() {
	// The root Addilytics middleware handles this route first.
	return new Response(null, { status: 404 });
}

Hash-only changes are ignored by default. Pass trackHashChanges on the browser component to count them. Restoring a page from the browser back-forward cache records a fresh pageview.

The relay defaults to /__addilytics. If you change it, the two option names differ but their values must match:

// Server
createAddilyticsMiddleware({
	endpoint: 'https://addilytics.example',
	mode: 'hybrid',
	relayPath: '/internal/analytics',
	siteKey: process.env.ADDILYTICS_KEY!
});

// Browser
<AddilyticsBrowser endpoint="/internal/analytics" mode="hybrid" />;

The browser sends only an event ID, timestamp, pathname, allowlisted campaign parameters, and the referrer origin. The middleware derives request metadata and forwards the event with the server-only site key. The relay rejects cross-origin requests and malformed payloads.

Custom events

The middleware puts a request-bound tracker in React Router's context. Loaders and actions can use it without reconstructing a Request:

import { addilyticsContext } from '@addilytics/react-router';

export async function action({ context }: Route.ActionArgs) {
	const order = await createOrder();
	await context.get(addilyticsContext).track('purchase', {
		props: { currency: 'USD', total: order.total }
	});
	return order;
}

The adapter accepts user and ip callbacks with the full middleware arguments. Explicit values passed to track take priority over user:

const analytics = createAddilyticsMiddleware({
	endpoint: 'https://addilytics.example',
	siteKey: process.env.ADDILYTICS_KEY!,
	user: ({ context }) => context.get(userContext).id,
	ip: ({ request }) => request.headers.get('x-real-ip')
});

Background delivery

If your deployment runtime has waitUntil, put its execution context into the React Router context returned by getLoadContext:

import { RouterContextProvider } from 'react-router';
import { addilyticsExecutionContext } from '@addilytics/react-router';

function getLoadContext(_request: Request, executionContext: ExecutionContext) {
	const context = new RouterContextProvider();
	context.set(addilyticsExecutionContext, executionContext);
	return context;
}

You can also pass a waitUntil resolver in the adapter options when your server adapter stores it elsewhere. Without waitUntil, the middleware waits for delivery before returning the response.

Vite setup

The Vite plugin can add the middleware when your root route doesn't already export middleware. Keep credentials in a .server.ts or .server.js module:

// app/addilytics.server.ts
import type { ReactRouterOptions } from '@addilytics/react-router';

export default {
	endpoint: 'https://addilytics.example',
	mode: 'hybrid',
	siteKey: process.env.ADDILYTICS_KEY!
} satisfies ReactRouterOptions;
// vite.config.ts
import { reactRouter } from '@react-router/dev/vite';
import { addilyticsReactRouter } from '@addilytics/react-router/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [reactRouter(), addilyticsReactRouter({ optionsModule: 'app/addilytics.server.ts' })]
});

The plugin edits the configured root route while Vite compiles the server. It doesn't patch emitted chunks, and it never imports the options module into the browser build. The build fails if the root already exports middleware, the options file isn't server-only, or the server build never loads the configured root. Compose by hand when your root already has middleware:

export const middleware: Route.MiddlewareFunction[] = [
	authAndSessionMiddleware,
	analytics,
	responseMiddleware
];