# PostHog react-react-router-7-data Example Project Repository: https://github.com/PostHog/context-mill Path: basics/react-react-router-7-data --- ## README.md # PostHog React Router 7 Data Mode example This is a [React Router 7](https://reactrouter.com) Data Mode example demonstrating PostHog integration with product analytics, session replay, feature flags, and error tracking. ## Features - **Product Analytics**: Track user events and behaviors - **Session Replay**: Record and replay user sessions - **Error Tracking**: Capture and track errors - **User Authentication**: Demo login system with PostHog user identification - **Client-side Tracking**: Examples of client-side tracking methods - **Data Mode**: React Router 7 data mode with client-side routing ## Getting Started ### 1. Install Dependencies ```bash npm install # or pnpm install ``` ### 2. Configure Environment Variables Create a `.env` file in the root directory: ```bash VITE_PUBLIC_POSTHOG_PROJECT_TOKEN=your_posthog_project_token VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com ``` Get your PostHog project token from your [PostHog project settings](https://app.posthog.com/project/settings). ### 3. Run the Development Server ```bash npm run dev # or pnpm dev ``` Open [http://localhost:5173](http://localhost:5173) with your browser to see the app. ## Project Structure ``` 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 initialization ``` ## Key Integration Points ### Client-side initialization (index.tsx) ```typescript 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', }); ``` ### User identification (AuthContext.tsx) The user is identified when the user logs in on the **client-side**. ```typescript 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. ### Event tracking (burrito.tsx) ```typescript posthog?.capture('burrito_considered', { total_considerations: updatedUser.burritoConsiderations, username: user.username, }); ``` ### Error tracking (root.tsx, profile.tsx) Errors are captured in two ways: 1. **Error boundary** - The `RootErrorBoundary` in `root.tsx` automatically captures unhandled React Router errors: ```typescript export function RootErrorBoundary() { const error = useRouteError(); const posthog = usePostHog(); if (error) { posthog.captureException(error); } // ... error UI } ``` 2. **Manual error capture** in components (profile.tsx): ```typescript posthog?.captureException(err); ``` ## Learn More - [PostHog Documentation](https://posthog.com/docs) - [React Router 7 Documentation](https://reactrouter.com) - [PostHog React Integration Guide](https://posthog.com/docs/libraries/react) --- ## .env.example ```example VITE_PUBLIC_POSTHOG_PROJECT_TOKEN= VITE_PUBLIC_POSTHOG_HOST= PROJECT_ID= ``` --- ## .react-router/types/+future.ts ```ts // Generated by React Router import "react-router"; declare module "react-router" { interface Future { v8_middleware: false } } ``` --- ## .react-router/types/+routes.ts ```ts // 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"); }; ``` --- ## .react-router/types/app/+types/root.ts ```ts // 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; 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"]; } ``` --- ## .react-router/types/app/routes/+types/home.ts ```ts // 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; 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"]; } ``` --- ## app/components/Header.tsx ```tsx 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 (
{user ? ( <> Welcome, {user.username}! ) : ( Not logged in )}
); } ``` --- ## app/contexts/AuthContext.tsx ```tsx 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; logout: () => void; setUser: (user: User) => void; } const AuthContext = createContext(undefined); const users: Map = new Map(); export function AuthProvider({ children }: { children: ReactNode }) { const posthog = usePostHog(); const [user, setUser] = useState(() => { 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 => { // 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 ( {children} ); } export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; } ``` --- ## app/root.tsx ```tsx 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 (
); } export function RootErrorBoundary() { const error = useRouteError(); const posthog = usePostHog(); if (error) { posthog.captureException(error); } if (isRouteErrorResponse(error)) { return ( <>

{error.status} {error.statusText}

{error.data}

); } else if (error instanceof Error) { return (

Error

{error.message}

The stack trace is:

{error.stack}
); } else { return

Unknown Error

; } } ``` --- ## app/routes.tsx ```tsx 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: , ErrorBoundary: RootErrorBoundary, children: [ { index: true, element: , }, { path: "burrito", element: , }, { path: "profile", element: , }, ], }, ]; ``` --- ## app/routes/burrito.tsx ```tsx 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 (

Burrito consideration zone

Take a moment to truly consider the potential of burritos.

{hasConsidered && (

Thank you for your consideration! Count: {user.burritoConsiderations}

)}

Consideration stats

Total considerations: {user.burritoConsiderations}

); } ``` --- ## app/routes/home.tsx ```tsx 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 (

Welcome back, {user.username}!

You are now logged in. Feel free to explore:

  • Consider the potential of burritos
  • View your profile and statistics
); } return (

Welcome to Burrito Consideration App

Please sign in to begin your burrito journey

setUsername(e.target.value)} placeholder="Enter any username" />
setPassword(e.target.value)} placeholder="Enter any password" />
{error &&

{error}

}

Note: This is a demo app. Use any username and password to sign in.

); } ``` --- ## app/routes/profile.tsx ```tsx 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 (

User Profile

Your Information

Username: {user.username}

Burrito Considerations: {user.burritoConsiderations}

Your Burrito Journey

{user.burritoConsiderations === 0 ? (

You haven't considered any burritos yet. Visit the Burrito Consideration page to start!

) : user.burritoConsiderations === 1 ? (

You've considered the burrito potential once. Keep going!

) : user.burritoConsiderations < 5 ? (

You're getting the hang of burrito consideration!

) : user.burritoConsiderations < 10 ? (

You're becoming a burrito consideration expert!

) : (

You are a true burrito consideration master! 🌯

)}
); } ``` --- ## index.html ```html React Router 7 Data Mode
``` --- ## index.tsx ```tsx 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( ); ``` --- ## vite.config.ts ```ts import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [react(), tsconfigPaths()], }); ``` ---