Intro
Your biggest client just requested early access to a new dashboard you've been building. The feature is ready but it's sitting in the same codebase that every other user hits. You don't want to create a separate branch, maintain two deployments, or worse, accidentally expose a half-finished UI to users who weren't supposed to see it.
So what do you do?
This is exactly the kind of problem feature flags were built for. Instead of branching your infrastructure or gating access with a tangle of if (user.id === 'acme-corp') checks scattered across your components, you wrap the feature in a flag. Acme Corp gets it. Everyone else sees nothing different. When you're ready to roll it out wider, you change a config without any redeploy or drama.
Feature flags (also called feature toggles) let you separate deployment from release. You merge and deploy code freely, but control who sees what from a dashboard. The feature lives in production but stays invisible until you say otherwise.
In this guide, you'll learn how flags work, when to use them, how tools compare, and how to implement them in a React app using PostHog — step by step.
Common Use Cases
Feature flags are a flexible primitive. The same mechanism serves different goals depending on how you configure them.
Beta releases and early access:
You have a new feature that's ready but not battle-tested at scale. Instead of shipping to everyone at once, you enable the flag for a small percentage of users let's say, 5% and watch your error rates. No spikes? Bump it to 25%, then 50%, then 100%. You get real production traffic as a signal without betting your entire user base on it.
A/B testing:
You want to know whether a redesigned checkout button increases conversions. You split your users, 50% see the old version, 50% see the new one. The flag handles stable assignment, meaning the same user always sees the same variant. Once you have enough data, you pick a winner and remove the flag.
Kill switches:
You've integrated a third-party payment provider. It works great, until it doesn't. Wrapping that integration in a flag means that when the provider has an outage at 2am, you flip a switch and fall back to the previous flow instantly. No hotfix. No emergency deploy. No waking up the team.
Entitlement gating:
Some features are only for paid users. Instead of hardcoding plan checks into your components, you set a flag that your billing system controls. Pro user? Flag is on. Free tier? Flag is off. Your UI just reads the flag and wouldn't care about the business logic behind it.
Feature Flag Tools: A Brief Comparison
| Tool | Best for | Free tier | A/B testing |
|---|---|---|---|
| PostHog | Full-stack with analytics | Yes | Built-in |
| LaunchDarkly | Enterprise, complex targeting | Limited | Add-on |
| Unleash | Self-hosted, open source | OSS | Basic |
| Flagsmith | Lightweight, open source option | Yes | Limited |
| GrowthBook | Experiment-first teams | Yes | Core focus |
PostHog:
PostHog is an open-source product analytics platform that ships feature flags as a first-class feature. You get flag management, percentage rollouts, user targeting, and A/B testing all in one place. The free tier is generous enough for most indie projects and small teams, and the React SDK is straightforward to integrate. This is what we'll use in the implementation section.
LaunchDarkly:
LaunchDarkly is the industry standard for enterprise feature flag management. It supports complex targeting rules, multi-environment flag syncing, and detailed audit logs. The tradeoff is cost, the free tier is limited, and it can feel like overkill for smaller projects.
Unleash:
Unleash is an open-source flag server you can self-host. If data privacy or cost is a concern and you have the infrastructure to manage it, Unleash gives you full control. The hosted cloud version is available too if you'd rather not run your own server.
Flagsmith:
Flagsmith is a clean, lightweight option that covers the basics well. It supports both cloud and self-hosted setups and has a usable free tier. A/B testing capabilities are limited compared to PostHog or GrowthBook, but if all you need is simple flag management, it does the job without much overhead.
GrowthBook:
GrowthBook is built specifically around experimentation. If running structured A/B tests and analysing statistical significance is your primary goal, GrowthBook is worth a serious look. Feature flags are supported but they're secondary to the experimentation workflow.
Implementing Feature Flags with PostHog and React
Now, for the fun part, we'll build a complete setup from scratch: installing PostHog, creating a flag, reading it in a component, identifying users, and targeting specific cohorts.
Step 1: Initialize React and Install the PostHog SDK
# Initialize React
npm create vite@latest feature-flag -- --template react-ts
# Install PostHog SDK
npm install --save posthog-js @posthog/react
Step 2: Initialise PostHog and wrap your app
Create a PostHog instance and wrap your root component with the provider. Your project API key lives in your PostHog project settings under Project > API Keys.
// main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { posthog } from "posthog-js";
import { PostHogProvider } from "@posthog/react";
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_TOKEN, {
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
defaults: "2026-01-30",
});
createRoot(document.getElementById("root")!).render(
<StrictMode>
<PostHogProvider client={posthog}>
<App />
</PostHogProvider>
</StrictMode>,
);
Step 3: Create and configure your flag in PostHog:
In your PostHog dashboard, go to Feature Flags and click New feature flag, selet the Targeted Releases option. Set the key to new_dashboard. This is what you'll reference in your code, so keep it lowercase with underscores.
Under Release conditions, you'll see an email filter already available. Add the email of the person you want to grant access to:
This works because PostHog always captures email as a default property when you call posthog.identify(). You don't need to wait for any prior user activity. The flag is configured and ready before anyone logs in. The moment and email that contains @acme-corp.com logs in and identify() runs with their email, PostHog matches them and the flag turns on.
Save the flag and leave it enabled.
Worth knowing: If you want to target by a custom property like plan or company, PostHog needs to have captured that property from a previous session before you can use it in a flag rule. Email gets around this because PostHog treats it as a known property from the first identify() call. We'll cover this limitation more in the best practices section
Step 4: Read the flag in a component:
Use the useFeatureFlagEnabled hook to check whether the flag is active for the current user
import { useFeatureFlagEnabled, usePostHog } from "@posthog/react";
import { useState } from "react";
function App() {
const [email, setEmail] = useState("");
const [isLoggedIn, setIsLoggedIn] = useState(false);
const posthog = usePostHog();
const showNewDashboard = useFeatureFlagEnabled("new_dashboard");
const handleLogin = () => {
if (email.trim()) {
posthog.setPersonPropertiesForFlags({ email });
// The user Id is a mock user_id, the real one will come from the Backend response
posthog.identify("user_123", {
email,
});
setIsLoggedIn(true);
}
};
if (isLoggedIn && showNewDashboard === undefined) return <p>Loading...</p>;
return (
<>
{!isLoggedIn ? (
<section
id="login-section"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
}}
>
<div
style={{
textAlign: "center",
padding: "20px",
border: "1px solid #ccc",
borderRadius: "8px",
width: "300px",
}}
>
<h1>Login</h1>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
style={{
width: "100%",
padding: "10px",
marginBottom: "20px",
border: "1px solid #ddd",
borderRadius: "4px",
fontSize: "16px",
boxSizing: "border-box",
}}
onKeyDown={(e) => e.key === "Enter" && handleLogin()}
/>
<button
onClick={handleLogin}
style={{
width: "100%",
padding: "10px",
backgroundColor: "#007bff",
color: "white",
border: "none",
borderRadius: "4px",
fontSize: "16px",
cursor: "pointer",
}}
>
Login
</button>
</div>
</section>
) : (
<section
id="dashboard"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
}}
>
<div style={{ textAlign: "center" }}>
<h1>
{showNewDashboard
? "Welcome Acme, This is a super secret mission"
: "Welcome"}
</h1>
<p>You are logged in as: {email}</p>
<button
onClick={() => {
setIsLoggedIn(false);
setEmail("");
}}
style={{
padding: "10px 20px",
backgroundColor: "#dc3545",
color: "white",
border: "none",
borderRadius: "4px",
fontSize: "16px",
cursor: "pointer",
marginTop: "20px",
}}
>
Logout
</button>
</div>
</section>
)}
</>
);
}
export default App;
Notice the undefined check. Before PostHog finishes loading flag configs, useFeatureFlagEnabled returns undefined, not false. Skipping this check causes a flash where users briefly see the wrong UI before the flag resolves.
Also, we can see the posthog.identify() in the handleLogin function. This is what connects the logged-in user to the flag rule you configured in Step 3.
When any email containing @acme-corp.com logs in, PostHog matches their email against the flag rule and enables the new_dashboard flag for their session. Every other user logs in, gets no match, and sees the legacy dashboard.
Step 5: Reset on logout:
When a user logs out, call posthog.reset() so the next user starts with a clean identity and doesn't inherit the previous session's flag evaluations. We will create a handleLogout function.
const handleLogout = () => {
posthog.reset()
setIsLoggedIn(false)
setEmail("")
}
// Then in the logout button
<button
onClick={handleLogout}
style={{
padding: "10px 20px",
backgroundColor: "#dc3545",
color: "white",
border: "none",
borderRadius: "4px",
fontSize: "16px",
cursor: "pointer",
marginTop: "20px",
}}
>
Logout
</button>
Best Practices
Name flags by intent, not implementation:
new_dashboard is clear. use_v2_component is not. Anyone reading the flag name in the PostHog dashboard or in code should immediately understand what it controls without having to dig through the codebase.
Always handle the loading state:
As shown in the implementation, useFeatureFlagEnabled returns undefined before flags have loaded. If you treat undefined as false, you risk flashing the wrong UI to users who should have seen the flagged version. Always render a skeleton or neutral state while flags are resolving.
Set a safe default:
If the PostHog SDK fails to load or the network is slow, your flag check should fall back to the old, stable behaviour. Never let a failed flag evaluation break your UI or leave users stuck on a blank screen.
Delete flags after full rollout:
A flag that stays in your codebase forever becomes invisible tech debt. Once a feature is stable and rolled out to everyone, remove the conditional, delete the flag from PostHog, and clean up the old code path. The rule of thumb: if the flag has been at 100% for more than one sprint, it should be gone.
Keep business logic out of flag checks:
The flag decides whether to show something. The component decides how to show it. Avoid nesting complex conditions inside the flag check itself. If you find yourself writing if (flagEnabled && user.plan === 'pro' && featureReady), that logic belongs in a separate function, not inline.
Gotchas
The property targeting chicken-and-egg problem:
This is the biggest PostHog-specific frustration. If you want to target users by a custom property like plan or company, PostHog needs to have captured that property from a previous session before the flag rule can use it. New users with no prior activity will not match the rule even if you pass the property in identify(). Email works around this because PostHog treats it as a known property from the very first identify() call. For anything beyond email targeting, consider a flags-first tool like LaunchDarkly or Unleash where flag evaluation is based entirely on what you pass at request time, not historical captured data.
UI flickering:
If you render the default UI first and swap to the flagged version after flags load, users see a flash. The fix is to wait for flags before rendering the relevant subtree, or evaluate flags server-side if you're using Next.js so the correct UI arrives in the initial render.
Flag explosion:
Teams that never clean up flags end up with dozens of permanent conditionals scattered across the codebase. Nobody remembers what half of them do. Keep a flag inventory, assign an owner to each flag, and treat flag removal as a first-class task in your sprint, not an afterthought.
Stale flag cache:
PostHog caches flag configs in memory after the initial load. If you update a flag rule in the dashboard while a user is mid-session, they won't see the change until they refresh or you manually call posthog.reloadFeatureFlags(). For kill switches where you need instant propagation, this is worth knowing upfront.
Conclusion
Feature flags are one of those tools that feel like a minor addition until the day you need to kill a broken feature at 2am without waking anyone up. Then they feel like the best decision you ever made.
The implementation is straightforward. Email targeting gets you most of the way there, and for anything more complex, you now know which tools to reach for.
Now go wrap something in a flag. Your future self, half-asleep with a production incident, will thank you.

Top comments (0)