# 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](https://tanstack.com/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 ```bash npm install ``` ### 2. Configure environment variables Create a `.env` file in the root directory: ```bash 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](https://app.posthog.com/project/settings). ### 3. Run the development server ```bash npm run dev ``` Open [http://localhost:3000](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: ```typescript import { PostHogProvider } from '@posthog/react' import { createRootRoute } from '@tanstack/react-router' const rootRoute = createRootRoute({ component: RootComponent, }) function RootComponent() { return ( {/* your app */} ) } ``` ### User identification (contexts/AuthContext.tsx) ```typescript import { usePostHog } from '@posthog/react' const posthog = usePostHog() posthog.identify(username, { username: username, }) ``` ### Event tracking (main.tsx - BurritoPage) ```typescript import { usePostHog } from '@posthog/react' const posthog = usePostHog() posthog.capture('burrito_considered', { total_considerations: count, username: username, }) ``` ### Error tracking (main.tsx - ProfilePage) ```typescript 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: ```typescript 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 - [PostHog Documentation](https://posthog.com/docs) - [TanStack Router Documentation](https://tanstack.com/router) - [TanStack Router Code-Based Routing](https://tanstack.com/router/latest/docs/framework/react/guide/code-based-routing) - [PostHog React Integration Guide](https://posthog.com/docs/libraries/react) --- ## .env.example ```example VITE_PUBLIC_POSTHOG_KEY= VITE_PUBLIC_POSTHOG_HOST= ``` --- ## .prettierignore ``` package-lock.json pnpm-lock.yaml yarn.lock ``` --- ## index.html ```html React TanStack Router - Code-Based
``` --- ## prettier.config.js ```js // @ts-check /** @type {import('prettier').Config} */ const config = { semi: false, singleQuote: true, trailingComma: "all", }; export default config; ``` --- ## public/robots.txt ```txt # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ``` --- ## src/contexts/AuthContext.tsx ```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; logout: () => void; incrementBurritoConsiderations: () => void; } const AuthContext = createContext(undefined); const users: Map = new Map(); export function AuthProvider({ children }: { children: ReactNode }) { // Use lazy initializer to read from localStorage only once on mount 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 posthog = usePostHog(); const login = async (username: string, password: string): Promise => { 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 ( {children} ); } 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 ```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 (
, }, ]} /> ) } // ============================================================================ // Header Component // ============================================================================ function Header() { const { user, logout } = useAuth() return (
{user ? ( <> Welcome, {user.username}! ) : ( Not logged in )}
) } // ============================================================================ // 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 (

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.

) } // ============================================================================ // 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 (

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}

) } // ============================================================================ // 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 (

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!

)}
) } // ============================================================================ // 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( , ) } // 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 ```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 ```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/, ''), }, }, }, } }) ``` ---