Chapter 199 · Omnibus Instrument Product Analytics
Subchapter 199.25
references/EXAMPLE-react-react-router-7-data.mdMarkdown20 KBView on GitHub
Repository: https://github.com/PostHog/context-mill Path: basics/react-react-router-7-data
This is a React Router 7 (opens in a new tab) Data Mode example demonstrating PostHog integration with product analytics, session replay, feature flags, and error tracking.
npm install
# or
pnpm installCreate a .env file in the root directory:
VITE_PUBLIC_POSTHOG_PROJECT_TOKEN=your_posthog_project_token
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.comGet your PostHog project token from your PostHog project settings (opens in a new tab).
npm run dev
# or
pnpm devOpen http://localhost:5173 (opens in a new tab) with your browser to see the app.
app/
├── components/
│ └── Header.tsx # Navigation header with auth state
├── contexts/
│ └── AuthContext.tsx # Authentication context with PostHog integration
├── routes/
│ ├── home.tsx # Home/Login page
│ ├── burrito.tsx # Demo feature page with event tracking
│ └── profile.tsx # User profile with error tracking demo
├── root.tsx # Root route with error boundary
└── routes.tsx # Route configuration
index.tsx # App entry point with PostHog initializationimport posthog from 'posthog-js';
import { PostHogProvider } from '@posthog/react'
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_PROJECT_TOKEN, {
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
defaults: '2026-01-30',
});
<PostHogProvider client={posthog}>
<RouterProvider router={router} />
</PostHogProvider>The user is identified when the user logs in on the client-side.
posthog.identify(username);
posthog.capture('user_logged_in');The session and distinct ID can be passed to the backend by including the X-POSTHOG-SESSION-ID and X-POSTHOG-DISTINCT-ID headers.
You should use these headers in the backend to identify events.
Important: do not identify users on the server-side.
posthog?.capture('burrito_considered', {
total_considerations: updatedUser.burritoConsiderations,
username: user.username,
});Errors are captured in two ways:
RootErrorBoundary in root.tsx automatically captures unhandled React Router errors:export function RootErrorBoundary() {
const error = useRouteError();
const posthog = usePostHog();
if (error) {
posthog.captureException(error);
}
// ... error UI
}posthog?.captureException(err);VITE_PUBLIC_POSTHOG_PROJECT_TOKEN=
VITE_PUBLIC_POSTHOG_HOST=
PROJECT_ID=// Generated by React Router
import "react-router";
declare module "react-router" {
interface Future {
v8_middleware: false
}
}// Generated by React Router
import "react-router"
declare module "react-router" {
interface Register {
pages: Pages
routeFiles: RouteFiles
routeModules: RouteModules
}
}
type Pages = {
"/": {
params: {};
};
};
type RouteFiles = {
"root.tsx": {
id: "root";
page: "/";
};
"routes/home.tsx": {
id: "routes/home";
page: "/";
};
};
type RouteModules = {
"root": typeof import("./app/root.tsx");
"routes/home": typeof import("./app/routes/home.tsx");
};// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../root.js")
type Info = GetInfo<{
file: "root.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../root.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../home.js")
type Info = GetInfo<{
file: "routes/home.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/home";
module: typeof import("../home.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}import { Link } from 'react-router';
import { useAuth } from '../contexts/AuthContext';
import { usePostHog } from '@posthog/react';
export default function Header() {
const { user, logout } = useAuth();
const posthog = usePostHog();
const handleLogout = () => {
posthog?.capture('user_logged_out');
posthog?.reset();
logout();
};
return (
<header className="header">
<div className="header-container">
<nav>
<Link to="/">Home</Link>
{user && (
<>
<Link to="/burrito">Burrito Consideration</Link>
<Link to="/profile">Profile</Link>
</>
)}
</nav>
<div className="user-section">
{user ? (
<>
<span>Welcome, {user.username}!</span>
<button onClick={handleLogout} className="btn-logout">
Logout
</button>
</>
) : (
<span>Not logged in</span>
)}
</div>
</div>
</header>
);
}
import { usePostHog } from '@posthog/react';
import { createContext, useContext, useState, type ReactNode } from 'react';
interface User {
username: string;
burritoConsiderations: number;
}
interface AuthContextType {
user: User | null;
login: (username: string, password: string) => Promise<boolean>;
logout: () => void;
setUser: (user: User) => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
const users: Map<string, User> = new Map();
export function AuthProvider({ children }: { children: ReactNode }) {
const posthog = usePostHog();
const [user, setUser] = useState<User | null>(() => {
if (typeof window === 'undefined') return null;
const storedUsername = localStorage.getItem('currentUser');
if (storedUsername) {
const existingUser = users.get(storedUsername);
if (existingUser) {
return existingUser;
}
}
return null;
});
const login = async (username: string, password: string): Promise<boolean> => {
// Client-side only fake auth - no server calls
if (!username || !password) {
return false;
}
let localUser = users.get(username);
if (!localUser) {
localUser = {
username,
burritoConsiderations: 0
};
users.set(username, localUser);
}
setUser(localUser);
localStorage.setItem('currentUser', username);
// Identifying the user once on login/sign up is enough.
posthog.identify(username);
posthog.capture('user_logged_in');
return true;
};
const logout = () => {
setUser(null);
localStorage.removeItem('currentUser');
};
const setUserState = (newUser: User) => {
setUser(newUser);
users.set(newUser.username, newUser);
};
return (
<AuthContext.Provider value={{ user, login, logout, setUser: setUserState }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
import { Outlet, useRouteError, isRouteErrorResponse } from "react-router";
import Header from "./components/Header";
import { AuthProvider } from "./contexts/AuthContext";
import "./globals.css";
import { usePostHog } from "@posthog/react";
export default function Root() {
return (
<AuthProvider>
<Header />
<main>
<Outlet />
</main>
</AuthProvider>
);
}
export function RootErrorBoundary() {
const error = useRouteError();
const posthog = usePostHog();
if (error) {
posthog.captureException(error);
}
if (isRouteErrorResponse(error)) {
return (
<>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data}</p>
</>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
import React from "react";
import type { RouteObject } from "react-router";
import Root, { RootErrorBoundary } from "./root";
import Home from "./routes/home";
import Burrito from "./routes/burrito";
import Profile from "./routes/profile";
export const routes: RouteObject[] = [
{
path: "/",
element: <Root />,
ErrorBoundary: RootErrorBoundary,
children: [
{
index: true,
element: <Home />,
},
{
path: "burrito",
element: <Burrito />,
},
{
path: "profile",
element: <Profile />,
},
],
},
];
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { useAuth } from '../contexts/AuthContext';
import { usePostHog } from '@posthog/react';
export default function BurritoPage() {
const { user, setUser } = useAuth();
const navigate = useNavigate();
const posthog = usePostHog();
const [hasConsidered, setHasConsidered] = useState(false);
useEffect(() => {
if (!user) {
navigate('/');
}
}, [user, navigate]);
if (!user) {
return null;
}
const handleConsideration = () => {
// Client-side only - no server calls
const updatedUser = {
...user,
burritoConsiderations: user.burritoConsiderations + 1
};
setUser(updatedUser);
setHasConsidered(true);
setTimeout(() => setHasConsidered(false), 2000);
// Capture burrito consideration event
posthog?.capture('burrito_considered', {
total_considerations: updatedUser.burritoConsiderations,
username: user.username,
});
};
return (
<div className="container">
<h1>Burrito consideration zone</h1>
<p>Take a moment to truly consider the potential of burritos.</p>
<div style={{ textAlign: 'center' }}>
<button
onClick={handleConsideration}
className="btn-burrito"
>
I have considered the burrito potential
</button>
{hasConsidered && (
<p className="success">
Thank you for your consideration! Count: {user.burritoConsiderations}
</p>
)}
</div>
<div className="stats">
<h3>Consideration stats</h3>
<p>Total considerations: {user.burritoConsiderations}</p>
</div>
</div>
);
}
import { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
export default function Home() {
const { user, login } = useAuth();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
const success = await login(username, password);
if (success) {
setUsername('');
setPassword('');
} else {
setError('Please provide both username and password');
}
};
if (user) {
return (
<div className="container">
<h1>Welcome back, {user.username}!</h1>
<p>You are now logged in. Feel free to explore:</p>
<ul>
<li>Consider the potential of burritos</li>
<li>View your profile and statistics</li>
</ul>
</div>
);
}
return (
<div className="container">
<h1>Welcome to Burrito Consideration App</h1>
<p>Please sign in to begin your burrito journey</p>
<form onSubmit={handleSubmit} className="form">
<div className="form-group">
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter any username"
/>
</div>
<div className="form-group">
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter any password"
/>
</div>
{error && <p className="error">{error}</p>}
<button type="submit" className="btn-primary">Sign In</button>
</form>
<p className="note">
Note: This is a demo app. Use any username and password to sign in.
</p>
</div>
);
}
import { useEffect } from 'react';
import { useNavigate } from 'react-router';
import { useAuth } from '../contexts/AuthContext';
import { usePostHog } from '@posthog/react';
export default function ProfilePage() {
const { user } = useAuth();
const navigate = useNavigate();
const posthog = usePostHog();
useEffect(() => {
if (!user) {
navigate('/');
}
}, [user, navigate]);
if (!user) {
return null;
}
const triggerTestError = () => {
try {
throw new Error('Test error for PostHog error tracking');
} catch (err) {
posthog?.captureException(err);
console.error('Captured error:', err);
alert('Error captured and sent to PostHog!');
}
};
return (
<div className="container">
<h1>User Profile</h1>
<div className="stats">
<h2>Your Information</h2>
<p><strong>Username:</strong> {user.username}</p>
<p><strong>Burrito Considerations:</strong> {user.burritoConsiderations}</p>
</div>
<div style={{ marginTop: '2rem' }}>
<button onClick={triggerTestError} className="btn-primary" style={{ backgroundColor: '#dc3545' }}>
Trigger Test Error (for PostHog)
</button>
</div>
<div style={{ marginTop: '2rem' }}>
<h3>Your Burrito Journey</h3>
{user.burritoConsiderations === 0 ? (
<p>You haven't considered any burritos yet. Visit the Burrito Consideration page to start!</p>
) : user.burritoConsiderations === 1 ? (
<p>You've considered the burrito potential once. Keep going!</p>
) : user.burritoConsiderations < 5 ? (
<p>You're getting the hang of burrito consideration!</p>
) : user.burritoConsiderations < 10 ? (
<p>You're becoming a burrito consideration expert!</p>
) : (
<p>You are a true burrito consideration master! 🌯</p>
)}
</div>
</div>
);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Router 7 Data Mode</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router";
import Root, { RootErrorBoundary } from "./app/root";
import Home from "./app/routes/home";
import Burrito from "./app/routes/burrito";
import Profile from "./app/routes/profile";
import posthog from 'posthog-js';
import { PostHogProvider } from '@posthog/react'
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_PROJECT_TOKEN, {
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
defaults: '2026-01-30',
});
const router = createBrowserRouter([
{
path: "/",
Component: Root,
ErrorBoundary: RootErrorBoundary,
children: [
{
index: true,
Component: Home,
},
{
path: "burrito",
Component: Burrito,
},
{
path: "profile",
Component: Profile,
},
],
},
]);
createRoot(document.getElementById("root")!).render(
<StrictMode>
<PostHogProvider client={posthog}>
<RouterProvider router={router} />
</PostHogProvider>
</StrictMode>
);
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [react(), tsconfigPaths()],
});