First, you’ll need to install posthog-js (opens in a new tab) and @posthog/react using your package manager. These packages allow you to capture client-side events.
If your site sets a Content-Security-Policy, it needs to allow PostHog. This applies to the snippet and to package installs alike: the SDK lazy-loads extra bundles (session replay, surveys) from PostHog’s CDN, and sends events to the ingestion host. PostHog serves from subdomains of posthog.com that change over time, so allow the wildcard:
script-src covers the snippet and the lazy-loaded bundles, connect-src covers event ingestion and feature flags, and worker-src covers session replay. The toolbar needs a few more (opens in a new tab), or use a reverse proxy (opens in a new tab) so everything is first-party. Failing to do so causes silent failures where capture and identify calls never send, so the integration looks complete while zero events arrive. Remember connect-src falls back to default-src, so default-src 'self' blocks event delivery even when the script itself is bundled.
In framework mode, you’ll also need to set posthog-js and @posthog/react as external packages in your vite.config.ts file to avoid SSR errors.
Add your environment variables to your .env.local file and to your hosting provider (e.g. Vercel, Netlify, AWS). You can find your project token and host in your project settings (opens in a new tab). If you’re using Vite, prefixing variable names with VITE_ ensures they are accessible in the frontend.
In framework mode, your app enters from the app/entry.client.tsx file. In this file, you’ll need to initialize the PostHog SDK and pass it to your app through the PostHogProvider context.
app/entry.client.tsx
jsx
import { startTransition, StrictMode } from "react";import { hydrateRoot } from "react-dom/client";import { HydratedRouter }
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 (opens in a new tab).
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
jsx
import { usePostHog } from '@posthog/react'function App() {
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:
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:
PostHog can capture exceptions thrown in your app through an error boundary. React Router in framework mode has a built-in error boundary that you can use to capture exceptions. You can create an error boundary by exporting ErrorBoundary from your app/root.tsx file.
app/root.tsx
jsx
import { usePostHog } from '@posthog/react'export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { const
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
jsx
import { PostHogCaptureOnViewed } from '@posthog/react'function App() { return ( <PostHogCaptureOnViewed name
Next, create a server-side middleware to help you capture server-side events. This middleware helps you achieve the following:
Initialize a PostHog client
Fetch the session and distinct ID from the X-POSTHOG-SESSION-ID and X-POSTHOG-DISTINCT-ID headers and pass them to your request as a context (opens in a new tab). This automatically identifies the user and session for you in all subsequent event captures.
Calls shutdown() on the PostHog client to ensure all events are sent before the request is completed.
Confirm that you can capture server-side events and see them in your PostHog project
At this point, you should be able to capture server-side events and see them in your PostHog project.
In a route, you can access the PostHog client from the context and capture an event. The middleware assigns the session ID and the distinct ID. This ensures that the system associates events with the correct user and session.
app/routes/api.checkout.ts
jsx
import type { PostHogContext } from "../lib/posthog-middleware";export async function action({ request
{/* Pass PostHog client through PostHogProvider */}
<PostHogProvider client={posthog}>
<StrictMode>
<HydratedRouter />
</StrictMode>
</PostHogProvider>,
);
});
To help PostHog track your user sessions across the client and server, you’ll need to add the tracing_headers: ['your-backend-hostname1.com', 'your-backend-hostname2.com', ...] option to your PostHog initialization. This adds the X-POSTHOG-DISTINCT-ID and X-POSTHOG-SESSION-ID headers to requests sent to the configured hostnames, which we’ll later use on the server-side.
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
jsx
useEffect(() => { posthog?.capture('test') // using optional chaining (recommended) if (posthog) { posthog.capture('test') // using an if statement }}, [posthog])
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 link to the new identified distinct ID, and all future captures on the same browser associate with the identified person.
posthog
=
usePostHog
();
posthog?.captureException(error);
// other error handling code...
return (
<div>
<h1>Something went wrong</h1>
<p>{error.message}</p>
</div>
);
}
This automatically captures exceptions thrown in your React Router app using the posthog.captureException() method.
=
"hero-banner"
>
<div>Your important content here</div>
</PostHogCaptureOnViewed>
)
}
With custom properties:
You can include additional properties with the event to provide more context:
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: