# 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',
});

<PostHogProvider client={posthog}>
  <RouterProvider router={router} />
</PostHogProvider>
```

### 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<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"];
}
```

---

## .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<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"];
}
```

---

## 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 (
    <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>
  );
}

```

---

## 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<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;
}

```

---

## 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 (
      <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>;
  }
}

```

---

## 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: <Root />,
    ErrorBoundary: RootErrorBoundary,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: "burrito",
        element: <Burrito />,
      },
      {
        path: "profile",
        element: <Profile />,
      },
    ],
  },
];


```

---

## 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 (
    <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>
  );
}

```

---

## 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 (
      <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>
  );
}

```

---

## 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 (
    <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&apos;t considered any burritos yet. Visit the Burrito Consideration page to start!</p>
        ) : user.burritoConsiderations === 1 ? (
          <p>You&apos;ve considered the burrito potential once. Keep going!</p>
        ) : user.burritoConsiderations < 5 ? (
          <p>You&apos;re getting the hang of burrito consideration!</p>
        ) : user.burritoConsiderations < 10 ? (
          <p>You&apos;re becoming a burrito consideration expert!</p>
        ) : (
          <p>You are a true burrito consideration master! 🌯</p>
        )}
      </div>
    </div>
  );
}

```

---

## index.html

```html
<!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>

```

---

## 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(
  <StrictMode>
    <PostHogProvider client={posthog}>
      <RouterProvider router={router} />
    </PostHogProvider>
  </StrictMode>
);


```

---

## 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()],
});


```

---

