React Router V7 data mode

This guide walks you through setting up PostHog for React Router V7 in data mode. If you're using React Router in another mode, find the guide for that mode in the React Router page. If you're using React with another framework, go to the React integration guide.

  1. Install client-side SDKs

    Required

    First, you'll need to install posthog-js and @posthog/react using your package manager. These packages allow you to capture client-side events.

    npm install --save posthog-js @posthog/react

  2. Add your environment variables

    Required

    Add your environment variables to your .env.local file and to your hosting provider (e.g. Vercel, Netlify, AWS). You can find your project API key and host in your project settings. If you're using Vite, including VITE_PUBLIC_ in their names ensures they are accessible in the frontend.

    .env.local
    VITE_PUBLIC_POSTHOG_KEY=<ph_project_api_key>
    VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

  3. Add the PostHogProvider to your app

    Required

    In data mode, you'll need to wrap your RouterProvider with the PostHogProvider context. This passes an initialized PostHog client to your app.

    app/index.tsx
    import { StrictMode } from "react";
    import { createRoot } from "react-dom/client";
    import { createBrowserRouter, RouterProvider } from "react-router";
    import Root, { RootErrorBoundary } from "./app/root";
    import posthog from 'posthog-js';
    import { PostHogProvider } from '@posthog/react'
    posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
    api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
    defaults: '2025-11-30',
    });
    const router = createBrowserRouter([...]);
    createRoot(document.getElementById("root")!).render(
    <StrictMode>
    {/* Pass PostHog client through PostHogProvider */}
    <PostHogProvider client={posthog}>
    <RouterProvider router={router} />
    </PostHogProvider>,
    );
    });

    This will initialize PostHog and pass it to your app through the PostHogProvider context.

  4. Verify client-side events are captured

    Checkpoint
    Confirm that you can capture client-side events and see them in your PostHog project

    At this point, you should be able to capture client-side events and see them in your PostHog project. This includes basic events like page views and button clicks that are autocaptured.

    You can also try to capture a custom event to verify it's working. You can access PostHog in any component using the usePostHog hook.

    TSX
    import { usePostHog } from '@posthog/react'
    function App() {
    const posthog = usePostHog()
    return <button onClick={() => posthog?.capture('button_clicked')}>Click me</button>
    }

    You should see these events in a minute or two in the activity tab.

  5. Access PostHog methods

    Required

    On the client-side, you can access the PostHog client using the usePostHog hook. This hook returns the initialized PostHog client, which you can use to call PostHog methods. For example:

    TSX
    import { usePostHog } from '@posthog/react'
    function App() {
    const posthog = usePostHog()
    return <button onClick={() => posthog?.capture('button_clicked')}>Click me</button>
    }

    For a complete list of available methods, see the posthog-js documentation.

  6. Identify your user

    Recommended

    Now that you can capture basic client-side events, you'll want to identify your user so you can associate users with captured events.

    Generally, you identify users when they log in or when they input some identifiable information (e.g. email, name, etc.). You can identify users by calling the identify method on the PostHog client:

    TSX
    export default function Login() {
    const { user, login } = useAuth();
    const posthog = usePostHog();
    const handleLogin = async (e: React.FormEvent) => {
    // existing code to handle login...
    const user = await login({ email, password });
    posthog?.identify(user.email,
    {
    email: user.email,
    name: user.name,
    }
    );
    posthog?.capture('user_logged_in');
    };
    return (
    <div>
    {/* ... existing code ... */}
    <button onClick={handleLogin}>Login</button>
    </div>
    );
    }

    PostHog automatically generates anonymous IDs for users before they're identified. When you call identify, a new identified person is created. All previous events tracked with the anonymous ID are linked to the new identified distinct ID, and all future captures on the same browser will be associated with the identified person.

  7. Create an error boundary

    Recommended

    PostHog can capture exceptions thrown in your app through an error boundary. React Router in data mode has a built-in error boundary that you can use to capture exceptions. You can create an error boundary by exporting RootErrorBoundary from your app/root.tsx file.

    app/root.tsx
    import { usePostHog } from '@posthog/react'
    export function RootErrorBoundary() {
    const error = useRouteError();
    const posthog = usePostHog();
    if (error) {
    posthog.captureException(error);
    }
    // other error handling code...
    }

    This will automatically capture exceptions thrown in your React Router app using the posthog.captureException() method.

  8. Set up server-side analytics

    Recommended

    Now that you've set up PostHog for React Router V7 in data mode, you can continue to set up server-side analytics. You can find our other SDKs in the SDKs page.

    To help PostHog track your user sessions across the client and server, you'll need to add the __add_tracing_headers: ['your-backend-domain1.com', 'your-backend-domain2.com', ...] option to your PostHog initialization:

    TSX
    posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
    api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
    defaults: '2025-11-30',
    __add_tracing_headers: [ window.location.host, 'localhost' ],
    });

    This will add the X-POSTHOG-DISTINCT-ID and X-POSTHOG-SESSION-ID headers to your requests, which you can later use on the server-side.

  9. Next steps

    Recommended

    Now that you've set up PostHog for React Router, you can start capturing events and exceptions in your app.

    To get the most out of PostHog, you should familiarize yourself with the following:

    • PostHog Web SDK docs: Learn more about the PostHog Web SDK and how to use it on the client-side.
    • PostHog Node SDK docs: Learn more about the PostHog Node SDK and how to use it on the server-side.
    • Identify users: Learn more about how to identify users in your app.
    • Group analytics: Learn more about how to use group analytics in your app.
    • PostHog AI: After capturing events, use PostHog AI to help you understand your data and build insights.


TypeError: Cannot read properties of undefined

If you see the error TypeError: Cannot read properties of undefined (reading '...') this is likely because you tried to call a posthog function when posthog was not initialized (such as during the initial render). On purpose, we still render the children even if PostHog is not initialized so that your app still loads even if PostHog can't load.

To fix this error, add a check that posthog has been initialized such as:

React
useEffect(() => {
posthog?.capture('test') // using optional chaining (recommended)
if (posthog) {
posthog.capture('test') // using an if statement
}
}, [posthog])

Typescript helps protect against these errors.

Tracking element visibility

The PostHogCaptureOnViewed component enables you to automatically capture events when elements scroll into view in the browser. This is useful for tracking impressions of important content, monitoring user engagement with specific sections, or understanding which parts of your page users are actually seeing.

The component wraps your content and sends a $element_viewed event to PostHog when the wrapped element becomes visible in the viewport. It only fires once per component instance.

Basic usage:

React
import { PostHogCaptureOnViewed } from '@posthog/react'
function App() {
return (
<PostHogCaptureOnViewed name="hero-banner">
<div>Your important content here</div>
</PostHogCaptureOnViewed>
)
}

With custom properties:

You can include additional properties with the event to provide more context:

React
<PostHogCaptureOnViewed
name="product-card"
properties={{
product_id: '123',
category: 'electronics',
price: 299.99
}}
>
<ProductCard />
</PostHogCaptureOnViewed>

Tracking multiple children:

Use trackAllChildren to track each child element separately. This is useful for galleries or lists where you want to know which specific items were viewed:

React
<PostHogCaptureOnViewed
name="product-gallery"
properties={{ gallery_type: 'featured' }}
trackAllChildren
>
<ProductCard id="1" />
<ProductCard id="2" />
<ProductCard id="3" />
</PostHogCaptureOnViewed>

When trackAllChildren is enabled, each child element sends its own event with a child_index property indicating its position.

Custom intersection observer options:

You can customize when elements are considered "viewed" by passing options to the IntersectionObserver:

React
<PostHogCaptureOnViewed
name="footer"
observerOptions={{
threshold: 0.5, // Element is 50% visible
rootMargin: '0px'
}}
>
<Footer />
</PostHogCaptureOnViewed>

The component passes all other props to the wrapper div, so you can add styling, classes, or other HTML attributes as needed.

Feature flags

PostHog's feature flags enable you to safely deploy and roll back new features as well as target specific users and groups with them.

There are two ways to implement feature flags in React:

  1. Using hooks.
  2. Using the <PostHogFeature> component.

Method 1: Using hooks

PostHog provides several hooks to make it easy to use feature flags in your React app.

HookDescription
useFeatureFlagEnabledReturns a boolean indicating whether the feature flag is enabled. This sends a $feature_flag_called event.
useFeatureFlagVariantKeyReturns the variant key of the feature flag. This sends a $feature_flag_called event.
useActiveFeatureFlagsReturns an array of active feature flags. This does not send a $feature_flag_called event.
useFeatureFlagPayloadReturns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey.

Example 1: Using a boolean feature flag

React
import { useFeatureFlagEnabled } from '@posthog/react'
function App() {
const showWelcomeMessage = useFeatureFlagEnabled('flag-key')
const payload = useFeatureFlagPayload('flag-key')
return (
<div className="App">
{
showWelcomeMessage ? (
<div>
<h1>Welcome!</h1>
<p>Thanks for trying out our feature flags.</p>
</div>
) : (
<div>
<h2>No welcome message</h2>
<p>Because the feature flag evaluated to false.</p>
</div>
)
}
</div>
);
}
export default App;

Example 2: Using a multivariate feature flag

React
import { useFeatureFlagVariantKey } from '@posthog/react'
function App() {
const variantKey = useFeatureFlagVariantKey('show-welcome-message')
let welcomeMessage = ''
if (variantKey === 'variant-a') {
welcomeMessage = 'Welcome to the Alpha!'
} else if (variantKey === 'variant-b') {
welcomeMessage = 'Welcome to the Beta!'
}
return (
<div className="App">
{
welcomeMessage ? (
<div>
<h1>{welcomeMessage}</h1>
<p>Thanks for trying out our feature flags.</p>
</div>
) : (
<div>
<h2>No welcome message</h2>
<p>Because the feature flag evaluated to false.</p>
</div>
)
}
</div>
);
}
export default App;

Example 3: Using a flag payload

Payload hook

The useFeatureFlagPayload hook does not send a $feature_flag_called event, which is required for the experiment to be tracked. To ensure the exposure event is sent, you should always use the useFeatureFlagPayload hook with either the useFeatureFlagEnabled or useFeatureFlagVariantKey hook.

React
import { useFeatureFlagPayload } from '@posthog/react'
function App() {
const variant = useFeatureFlagEnabled('show-welcome-message')
const payload = useFeatureFlagPayload('show-welcome-message')
return (
<>
{
variant ? (
<div className="welcome-message">
<h2>{payload?.welcomeTitle}</h2>
<p>{payload?.welcomeMessage}</p>
</div>
) : <div>
<h2>No custom welcome message</h2>
<p>Because the feature flag evaluated to false.</p>
</div>
}
</>
)
}

Method 2: Using the PostHogFeature component

The PostHogFeature component simplifies code by handling feature flag related logic.

It also automatically captures metrics, like how many times a user interacts with this feature.

Note: You still need the PostHogProvider at the top level for this to work.

Here is an example:

React
import { PostHogFeature } from '@posthog/react'
function App() {
return (
<PostHogFeature flag='show-welcome-message' match={true}>
<div>
<h1>Hello</h1>
<p>Thanks for trying out our feature flags.</p>
</div>
</PostHogFeature>
)
}
  • The match on the component can be either true, or the variant key, to match on a specific variant.

  • If you also want to show a default message, you can pass these in the fallback attribute.

If you wish to customise logic around when the component is considered visible, you can pass in visibilityObserverOptions to the feature. These take the same options as the IntersectionObserver API. By default, we use a threshold of 0.1.

Payloads

If your flag has a payload, you can pass a function to children whose first argument is the payload. For example:

React
import { PostHogFeature } from '@posthog/react'
function App() {
return (
<PostHogFeature flag='show-welcome-message' match={true}>
{(payload) => {
return (
<div>
<h1>{payload.welcomeMessage}</h1>
<p>Thanks for trying out our feature flags.</p>
</div>
)
}}
</PostHogFeature>
)
}

Request timeout

You can configure the feature_flag_request_timeout_ms parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds.

JavaScript
posthog.init('<ph_project_api_key>', {
api_host: 'https://us.i.posthog.com',
defaults: '2025-11-30'
feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds).
}
)

Error handling

When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler:

JavaScript
function handleFeatureFlag(client, flagKey, distinctId) {
try {
const isEnabled = client.isFeatureEnabled(flagKey, distinctId);
console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`);
return isEnabled;
} catch (error) {
console.error(`Error fetching feature flag '${flagKey}': ${error.message}`);
// Optionally, you can return a default value or throw the error
// return false; // Default to disabled
throw error;
}
}
// Usage example
try {
const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123');
if (flagEnabled) {
// Implement new feature logic
} else {
// Implement old feature logic
}
} catch (error) {
// Handle the error at a higher level
console.error('Feature flag check failed, using default behavior');
// Implement fallback logic
}

Bootstrapping flags

Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag.

To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. After the SDK fetches feature flags from PostHog, it will use those flag values instead of bootstrapped ones.

For details on how to implement bootstrapping, see our bootstrapping guide.

Experiments (A/B tests)

Since experiments use feature flags, the code for running an experiment is very similar to the feature flags code:

React
// You can either use the `useFeatureFlagVariantKey` hook,
// or you can use the feature flags component - /docs/libraries/react#feature-flags-react-component
// Method one: using the useFeatureFlagVariantKey hook
import { useFeatureFlagVariantKey } from '@posthog/react'
function App() {
const variant = useFeatureFlagVariantKey('experiment-feature-flag-key')
if (variant == 'variant-name') {
// do something
}
}
// Method two: using the feature flags component
import { PostHogFeature } from '@posthog/react'
function App() {
return (
<PostHogFeature flag='experiment-feature-flag-key' match={'variant-name'}>
<!-- the component to show -->
</PostHogFeature>
)
}
// You can also test your code by overriding the feature flag:
// e.g., posthog.featureFlags.overrideFeatureFlags({ flags: {'experiment-feature-flag-key': 'test'}})

It's also possible to run experiments without using feature flags.

Community questions

Was this page useful?

Questions about this page? or post a community question.