Remix is a full stack web framework built on React with a specific focus on following web standards.
In this tutorial, we show you how to add PostHog to your Remix app (on both the client and server side), implement custom event capture, capture pageviews, and use feature flags.
Creating a Remix app
First, make sure to install a Node version greater than 20. After confirming, run the following command to create a basic app and choose the default options.
Terminal
npx create-remix@latest remix-tutorial
Our app is going to be a basic blog with a few pages we can move between.
We start by creating a top-level json folder in our newly created remix-tutorial folder. Our list of blog posts lives in a data.json file in this folder. Each contains a slug, title, and content.
JSON
{
"posts":[
{
"slug":"my-first-post",
"title":"My First Post",
"content":"This is a test post, how exciting!"
},
{
"slug":"posthog-is-cool",
"title":"PostHog is cool",
"content":"Go checkout PostHog for all your product tool needs!"
}
]
}
Next, we rework the home page to show these posts. In app/routes/_index.tsx, we create a loader that gets the data.json file, reads it, and returns the posts. In the client component, we use a map to list and link to each of them.
Next, we add the pages for these blog posts. In the app/routes folder, create a posts.$slug.tsx file. In this folder, we use the same pattern to load the posts and add a filter to match the slug. In the component, we show the content for the post and a like button we will use later.
Finally, go to vite.config.ts and add posthog-js and posthog-js/react as external packages. This ensures you app builds correctly when you're ready to deploy.
TypeScript
// vite.config.ts
// ... imports and rest of config
exportdefaultdefineConfig({
plugins:[
// ... plugins
],
ssr:{
noExternal:["posthog-js","posthog-js/react"],
},
});
After relaunching your app, PostHog begins autocapturing pageviews, clicks, session replays (if you enable them), and more.
Capturing custom events
Although PostHog autocaptures clicks, we can set up custom events to capture more specific actions. For example, we can set up a liked_post event for the like button.
To do this, go to routes/posts.$slug.tsx, import the usePostHog hook, and then set up a posthog.capture() call in place of the console.log().
Now, every time a user clicks this button, it captures a liked_post event with a custom post property.
Setting up feature flags
Next, we can add a feature flag to control a new version of the like button we want to try out.
To this, start by going to the feature flags tab in PostHog and clicking New feature flag. Name it new-button-text, set the rollout to 100% of users, fill out any other details, and press Save.
In the posts.$slug.tsx file, set up a useState for the button content, and a useEffect for flag evaluation. We conditionally render the button text based on the flag response.
Our post page now shows the updated button, which you can toggle remotely with the flag in PostHog.
Adding PostHog on the server side
Remix is a full stack framework, parts of it run on both the client and server side. So far, we’ve only used PostHog on the client side and the JavaScript Web library we installed won’t work on the server side. Instead, we must use the posthog-node SDK. We can start by installing it:
Terminal
npm i posthog-node
Next, we create a posthog.js file in the app folder. In it, we set up an initialization of the posthog-node client. We also set up logic to return the existing client if it is already initialized. Again, you need your project API key and instance address from your project settings.
Capturing an event with PostHog on the server side
To use this, we go back to posts.$slug.tsx and implement a server side event capture in the loader function.
An important difference between the frontend and backend implementations is that the backend requires a distinct_id while the frontend automatically handles it. To get a backend distinct_id, we check for the distinct_id in the request’s cookies, and if it doesn’t exist, we create a new one. We then use this value to capture an event with the imported PostHogClient() as well as pass it in the posts JSON for future use on the client side.
When you refresh the page now, you see a server_side_event_name event captured along with the others.
Combining frontend and backend distinct IDs
Lastly, since PostHog automatically creates a distinct_id on the frontend, we must connect it to the backend distinct_id (if they are different). This ensures we are consistently tracking users and that they get a consistent experience.
In the initial useEffect in our component, we check if the distinct_id values are different, and if so, use posthog.identify to combine them.
TypeScript
// app/routes/posts.$slug.tsx
// ... imports, type, loader
exportdefaultfunctionPostSlug(){
const post =useLoaderData<Post>();
const[buttonText, setButtonText]=useState('Like this post');
const flag = posthog.isFeatureEnabled('new-button-text');
if(flag){
setButtonText('This is a very cool post')
}
},[]);
// ... rest of the component
Using server side feature flags
Using PostHog on the server side enables us to evaluate flags before the page loads. This prevents the "flicker" of the flags loading and changing page elements.
To set this up, we use the server side isFeatureEnabled() method with the flag key and distinctId then add a new buttonText key and value to the post JSON like this:
With the buttonText included in the post JSON, we can rework our component to remove the buttonText state and client side isFeatureEnabled. This simplifies our component, which now looks like this: