placebo.mk/frontend/.claude/skills/posthog-integration-react-tanstack-router-code-based/references/EXAMPLE.md
2026-03-01 01:36:11 +01:00

19 KiB

PostHog React with TanStack Router (code-based) Example Project

Repository: https://github.com/PostHog/context-mill Path: basics/react-tanstack-router-code-based


README.md

PostHog TanStack Router Example (Code-Based Routing)

This is a React and TanStack Router example demonstrating PostHog integration with product analytics, session replay, and error tracking. This example uses code-based routing where routes are defined programmatically.

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: Pure client-side React implementation
  • Reverse proxy: PostHog ingestion through Vite proxy

Getting started

1. Install dependencies

npm install

2. Configure environment variables

Create a .env file in the root directory:

VITE_PUBLIC_POSTHOG_KEY=your_posthog_project_api_key
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

Get your PostHog API key from your PostHog project settings.

3. Run the development server

npm run dev

Open http://localhost:3000 with your browser to see the app.

Project structure

src/
├── contexts/
│   └── AuthContext.tsx    # Authentication context with PostHog integration
├── main.tsx               # App entry point with all routes defined in code
├── reportWebVitals.ts     # Performance monitoring
└── styles.css             # Global styles

Key integration points

PostHog provider setup (main.tsx)

PostHog is initialized using PostHogProvider from @posthog/react. The provider wraps the entire app in the root route component:

import { PostHogProvider } from '@posthog/react'
import { createRootRoute } from '@tanstack/react-router'

const rootRoute = createRootRoute({
  component: RootComponent,
})

function RootComponent() {
  return (
    <PostHogProvider
      apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY!}
      options={{
        api_host: '/ingest',
        ui_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST || 'https://us.posthog.com',
        defaults: '2026-01-30',
        capture_exceptions: true,
        debug: import.meta.env.DEV,
      }}
    >
      {/* your app */}
    </PostHogProvider>
  )
}

User identification (contexts/AuthContext.tsx)

import { usePostHog } from '@posthog/react'

const posthog = usePostHog()

posthog.identify(username, {
  username: username,
})

Event tracking (main.tsx - BurritoPage)

import { usePostHog } from '@posthog/react'

const posthog = usePostHog()

posthog.capture('burrito_considered', {
  total_considerations: count,
  username: username,
})

Error tracking (main.tsx - ProfilePage)

posthog.captureException(error)

TanStack Router details

This example uses TanStack Router with code-based routing. Key details:

  1. Client-side only: No server-side logic, no API routes, no posthog-node
  2. Code-based routing: All routes defined in main.tsx using createRoute() and createRootRoute()
  3. Manual route tree: Routes connected with addChildren() method
  4. Standard hooks: Uses useNavigate() from @tanstack/react-router
  5. Vite proxy: Uses Vite's proxy config for PostHog calls
  6. Environment variables: Uses import.meta.env.VITE_*
  7. PostHog provider: Uses PostHogProvider from @posthog/react in root route

Code-based vs File-based routing

This example demonstrates code-based routing, where routes are defined programmatically:

import { createRoute, createRootRoute, createRouter } from '@tanstack/react-router'

const rootRoute = createRootRoute({ component: RootComponent })

const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/',
  component: Home,
})

const burritoRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/burrito',
  component: BurritoPage,
})

const routeTree = rootRoute.addChildren([indexRoute, burritoRoute])

const router = createRouter({ routeTree })

For file-based routing (auto-generated from file structure), see the react-tanstack-router-file-based example.

Learn more


.env.example

VITE_PUBLIC_POSTHOG_KEY=<ph_project_api_key>
VITE_PUBLIC_POSTHOG_HOST=<ph_client_api_host>


.prettierignore

package-lock.json
pnpm-lock.yaml
yarn.lock


index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="React TanStack Router code-based routing example"
    />
    <link rel="apple-touch-icon" href="/logo192.png" />
    <link rel="manifest" href="/manifest.json" />
    <title>React TanStack Router - Code-Based</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>


prettier.config.js

//  @ts-check

/** @type {import('prettier').Config} */
const config = {
  semi: false,
  singleQuote: true,
  trailingComma: "all",
};

export default config;


public/robots.txt

# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:


src/contexts/AuthContext.tsx

import { createContext, useContext, useState, type ReactNode } from 'react';
import { usePostHog } from '@posthog/react';

interface User {
  username: string;
  burritoConsiderations: number;
}

interface AuthContextType {
  user: User | null;
  login: (username: string, password: string) => Promise<boolean>;
  logout: () => void;
  incrementBurritoConsiderations: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

const users: Map<string, User> = new Map();

export function AuthProvider({ children }: { children: ReactNode }) {
  // Use lazy initializer to read from localStorage only once on mount
  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 posthog = usePostHog();

  const login = async (username: string, password: string): Promise<boolean> => {
    if (!username || !password) {
      return false;
    }

    // Get or create user in local map
    let user = users.get(username);
    const isNewUser = !user;

    if (!user) {
      user = { username, burritoConsiderations: 0 };
      users.set(username, user);
    }

    setUser(user);
    localStorage.setItem('currentUser', username);

    // Identify user in PostHog using username as distinct ID
    posthog.identify(username, {
      username: username,
      isNewUser: isNewUser,
    });

    // Capture login event
    posthog.capture('user_logged_in', {
      username: username,
      isNewUser: isNewUser,
    });

    return true;
  };

  const logout = () => {
    // Capture logout event before resetting
    posthog.capture('user_logged_out');
    posthog.reset();

    setUser(null);
    localStorage.removeItem('currentUser');
  };

  const incrementBurritoConsiderations = () => {
    if (user) {
      user.burritoConsiderations++;
      users.set(user.username, user);
      setUser({ ...user });
    }
  };

  return (
    <AuthContext.Provider value={{ user, login, logout, incrementBurritoConsiderations }}>
      {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;
}


src/main.tsx

import { StrictMode, useState } from 'react'
import ReactDOM from 'react-dom/client'
import {
  Link,
  Outlet,
  RouterProvider,
  createRootRoute,
  createRoute,
  createRouter,
  useNavigate,
} from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import { PostHogProvider, usePostHog } from '@posthog/react'

import { AuthProvider, useAuth } from './contexts/AuthContext'
import './styles.css'
import reportWebVitals from './reportWebVitals'

// ============================================================================
// Root Route
// ============================================================================

const rootRoute = createRootRoute({
  component: RootComponent,
})

function RootComponent() {
  return (
    <PostHogProvider
      apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY!}
      options={{
        api_host: '/ingest',
        ui_host:
          import.meta.env.VITE_PUBLIC_POSTHOG_HOST || 'https://us.posthog.com',
        defaults: '2026-01-30',
        capture_exceptions: true,
        debug: import.meta.env.DEV,
      }}
    >
      <AuthProvider>
        <Header />
        <main>
          <Outlet />
        </main>
        <TanStackDevtools
          config={{
            position: 'bottom-right',
          }}
          plugins={[
            {
              name: 'Tanstack Router',
              render: <TanStackRouterDevtoolsPanel />,
            },
          ]}
        />
      </AuthProvider>
    </PostHogProvider>
  )
}

// ============================================================================
// Header Component
// ============================================================================

function Header() {
  const { user, logout } = useAuth()

  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={logout} className="btn-logout">
                Logout
              </button>
            </>
          ) : (
            <span>Not logged in</span>
          )}
        </div>
      </div>
    </header>
  )
}

// ============================================================================
// Index Route (Home Page)
// ============================================================================

const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/',
  component: Home,
})

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('')

    try {
      const success = await login(username, password)
      if (success) {
        setUsername('')
        setPassword('')
      } else {
        setError('Please provide both username and password')
      }
    } catch (err) {
      console.error('Login failed:', err)
      setError('An error occurred during login')
    }
  }

  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>
  )
}

// ============================================================================
// Burrito Route
// ============================================================================

const burritoRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/burrito',
  component: BurritoPage,
})

function BurritoPage() {
  const { user, incrementBurritoConsiderations } = useAuth()
  const navigate = useNavigate()
  const posthog = usePostHog()
  const [hasConsidered, setHasConsidered] = useState(false)

  // Redirect to home if not logged in
  if (!user) {
    navigate({ to: '/' })
    return null
  }

  const handleConsideration = () => {
    incrementBurritoConsiderations()
    setHasConsidered(true)
    setTimeout(() => setHasConsidered(false), 2000)

    // Capture burrito consideration event
    console.log('posthog', posthog)
    posthog.capture('burrito_considered', {
      total_considerations: user.burritoConsiderations + 1,
      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>
  )
}

// ============================================================================
// Profile Route
// ============================================================================

const profileRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/profile',
  component: ProfilePage,
})

function ProfilePage() {
  const { user } = useAuth()
  const navigate = useNavigate()
  const posthog = usePostHog()

  // Redirect to home if not logged in
  if (!user) {
    navigate({ to: '/' })
    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>
  )
}

// ============================================================================
// Route Tree & Router Setup
// ============================================================================

const routeTree = rootRoute.addChildren([indexRoute, burritoRoute, profileRoute])

const router = createRouter({
  routeTree,
  context: {},
  defaultPreload: 'intent',
  scrollRestoration: true,
  defaultStructuralSharing: true,
  defaultPreloadStaleTime: 0,
})

// Register the router instance for type safety
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

// ============================================================================
// Render the App
// ============================================================================

const rootElement = document.getElementById('app')
if (rootElement && !rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement)
  root.render(
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>,
  )
}

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()


src/reportWebVitals.ts

const reportWebVitals = (onPerfEntry?: () => void) => {
  if (onPerfEntry && onPerfEntry instanceof Function) {
    import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
      onCLS(onPerfEntry)
      onINP(onPerfEntry)
      onFCP(onPerfEntry)
      onLCP(onPerfEntry)
      onTTFB(onPerfEntry)
    })
  }
}

export default reportWebVitals


vite.config.ts

import { defineConfig, loadEnv } from 'vite'
import viteReact from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

import { fileURLToPath, URL } from 'node:url'

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')

  return {
    plugins: [viteReact(), tailwindcss()],
    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url)),
      },
    },
    server: {
      proxy: {
        '/ingest': {
          target: env.VITE_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/ingest/, ''),
        },
      },
    },
  }
})