Implementing Custom Feature Flags with Vite + React
Feature Flags are a technique for toggling features on/off without deploying new code. While third-party services like LaunchDarkly, Unleash, and Flagsmith are popular, you can build your own for simple use cases.
I created a demo project implementing custom feature flags with Vite 7 + React 19 + TanStack Router.
- GitHub: vite-feature-flag-demo
Benefits of Feature Flags
| Benefit | Description |
|---|---|
| Continuous Integration | Safely merge incomplete features to the main branch |
| Gradual Rollouts | Release features to a subset of users |
| A/B Testing | Test different implementations with real users |
| Instant Rollback | Disable problematic features without redeployment |
| Trunk-Based Development | Eliminate long-lived feature branches |
Trunk-Based Development Flow
A comparison between traditional development flow and trunk-based development with feature flags.
| Aspect | Traditional Flow | Trunk-Based Development |
|---|---|---|
| Branch Strategy | Long-lived feature branches | Merge directly to main branch |
| PR Size | Large PRs (weeks of changes) | Small PRs (frequent merges) |
| Merge Frequency | Once when feature is complete | Daily to every few days |
| Risk | High conflict risk at merge time | Low conflict, always deployable |
| Feature Release | Merge = Release | Release when flag is ON |
With feature flags, you can merge incomplete features to the main branch. Keep the flag OFF and users won’t see it. When it’s ready, just turn the flag ON to release.
Why Build a Custom Implementation?
| Aspect | Custom Implementation | Third-Party Service |
|---|---|---|
| Cost | Free | $50-500+/month |
| Complexity | Simple | Complex SDK integration |
| Control | Full ownership | Vendor dependency |
| Features | Basic (extensible) | Advanced (targeting, analytics) |
| Latency | None (client-side) | Network request required |
| Data Location | Your infrastructure | May lack regional availability |
For simple use cases, a custom implementation is sufficient. Some organizations also have restrictions on external SaaS usage, or data residency requirements (regional availability) may be a concern. In such cases, a custom implementation is a viable option.
For advanced targeting and analytics, consider third-party services.
Tech Stack
Here’s the tech stack used in this demo project.
| Technology | Version |
|---|---|
| Vite | 7.3.1 |
| React | 19.2.4 |
| TypeScript | 5.9.3 |
| TanStack Router | 1.158.0 |
| Turborepo | 2.8.3 |
| Biome | 2.3.14 |
| pnpm | 10.9.0 |
| Node.js | 24.13.0 |
Project Structure
A monorepo structure using pnpm workspaces and Turborepo.
apps/web - Main React App
| File | Description |
|---|---|
src/routes/__root.tsx | Root layout (FeatureFlagProvider placement) |
src/routes/index.tsx | Home page (demo UI) |
src/main.tsx | Entry point |
vite.config.ts | Vite configuration |
packages/feature-flags - Feature Flag Library
| File | Description |
|---|---|
src/index.ts | Public API exports |
src/types.ts | TypeScript type definitions |
src/FeatureFlagContext.ts | React Context definition |
src/FeatureFlagProvider.tsx | Provider component |
src/useFeatureFlag.ts | Custom React Hooks |
Root Configuration Files
| File | Description |
|---|---|
turbo.json | Turborepo pipeline configuration |
pnpm-workspace.yaml | pnpm workspace definition |
Two Patterns
This project implements feature flags using two distinct patterns.
| Pattern | Resolved At | Use Case |
|---|---|---|
| React Context (Runtime) | Runtime | A/B testing, user targeting, dynamic toggles |
| Environment Variables (Build-time) | Build time | Per-environment config, CI/CD integration |
Environment Variable Pattern
This pattern uses Vite’s environment variables to embed flags at build time. Use the VITE_FF_ prefix.
# .env
VITE_FF_EXPERIMENTAL_CHECKOUT=true
VITE_FF_ANALYTICS_DASHBOARD=true
VITE_FF_MAINTENANCE_MODE=falseimport { isEnvFlagEnabled } from "@demo/feature-flags";
if (isEnvFlagEnabled("maintenanceMode")) {
return <MaintenancePage />;
}Environment variable names are automatically converted from SCREAMING_SNAKE_CASE to camelCase.
| Environment Variable | Parsed Flag Name |
|---|---|
VITE_FF_NEW_DASHBOARD | newDashboard |
VITE_FF_DARK_MODE | darkMode |
Choosing the Right Pattern
| Aspect | React Context | Environment Variables |
|---|---|---|
| Change without rebuild | Yes | No |
| User-specific targeting | Yes | No |
| Per-environment config | Complex | Native support |
| Bundle size impact | Minimal | None (dead code elimination) |
You can combine both patterns for maximum flexibility.
import { useFeatureFlag, isEnvFlagEnabled } from "@demo/feature-flags";
function MyComponent() {
// Build-time: Is this feature available in this environment?
const isFeatureAvailable = isEnvFlagEnabled("newFeature");
// Runtime: Is this user enrolled in the feature?
const isUserEnrolled = useFeatureFlag("newFeatureRollout");
if (isFeatureAvailable && isUserEnrolled) {
return <NewFeature />;
}
return <LegacyFeature />;
}Implementation Details
Feature Flag Library Structure
A lightweight implementation using React’s Context API.
Type Definitions
// Feature flags are a simple key-value map
type FeatureFlags = Record<string, boolean>;
// Context value provided to consumers
type FeatureFlagContextValue = {
flags: FeatureFlags;
setFlag: (key: string, value: boolean) => void;
isEnabled: (key: string) => boolean;
};Provider Component
import { FeatureFlagProvider } from "@demo/feature-flags";
const defaultFlags = {
newDashboard: true, // Enabled by default
darkMode: false, // Disabled by default
betaFeature: false, // Disabled by default
};
function App() {
return (
<FeatureFlagProvider flags={defaultFlags}>
<YourApp />
</FeatureFlagProvider>
);
}Custom Hooks
import { useFeatureFlag } from "@demo/feature-flags";
function Dashboard() {
const isNewDashboardEnabled = useFeatureFlag("newDashboard");
if (isNewDashboardEnabled) {
return <NewDashboard />;
}
return <LegacyDashboard />;
}Common Patterns
Pattern 1: Component Replacement
function MyFeature() {
const useNewImplementation = useFeatureFlag("newImplementation");
return useNewImplementation ? <NewComponent /> : <OldComponent />;
}Pattern 2: Conditional Rendering
function Header() {
const showBetaBadge = useFeatureFlag("betaFeature");
return (
<header>
<h1>My App</h1>
{showBetaBadge && <span className="badge">BETA</span>}
</header>
);
}Pattern 3: Behavior Modification
function SubmitButton() {
const useAsyncSubmit = useFeatureFlag("asyncSubmit");
const handleClick = () => {
if (useAsyncSubmit) {
submitAsync();
} else {
submitSync();
}
};
return <button onClick={handleClick}>Submit</button>;
}Security Considerations
Limitations of Client-Side Feature Flags
This implementation uses client-side feature flags. There are important security considerations.
| Consideration | Description |
|---|---|
| All code is included in the bundle | Even when a flag is OFF, protected component code is included in the JavaScript bundle |
| Flags can be bypassed | Users can modify React state via browser DevTools to enable any flag |
| Source code is visible | ”Hidden” feature code can be seen through source maps or bundle analysis |
In other words, even with flags OFF, all code in the built JavaScript bundle—including /admin and /beta components and feature flag logic—is visible to users.
Protection Level Comparison
| Level | Method | Protects Against | Use Case |
|---|---|---|---|
| 1. UI Toggle (this demo) | Conditional rendering | Casual users | UX experiments, gradual rollouts |
| 2. Lazy Loading | React.lazy() + dynamic import | Light code inspection | Slightly better, but URL still accessible |
| 3. Server-Side Auth | API authorization checks | Unauthorized data access | Required for sensitive data |
| 4. Build-Time Exclusion | Environment variables at build | Code exposure | Separate builds per environment |
Recommended Architecture
| Layer | Role | Notes |
|---|---|---|
| Client-Side | UI display control only (UX purposes) | Not for security. Consider all client code as public information |
| Server-Side | Authentication, authorization, data filtering | Verify user identity, check permissions per request, evaluate flags server-side, return only permitted data |
When Client-Side Flags Are Appropriate
- A/B testing (UI variations)
- Gradual rollout of UI changes
- Beta feature previews (non-sensitive)
- Dark mode, theme toggles
- UI layout experiments
When Server-Side Protection Is Required
- Admin panels with sensitive data
- Premium/paid features
- User data access control
- Any feature with security implications
Production Considerations
This demo is intentionally simple. For production use, consider the following.
Flag Persistence
const [flags, setFlags] = useState(() => {
const saved = localStorage.getItem("featureFlags");
return saved ? JSON.parse(saved) : defaultFlags;
});
useEffect(() => {
localStorage.setItem("featureFlags", JSON.stringify(flags));
}, [flags]);Remote Flag Fetching
useEffect(() => {
fetch("/api/feature-flags")
.then((res) => res.json())
.then((remoteFlags) => setFlags(remoteFlags));
}, []);User-Based Targeting
const flags = {
betaFeature: user.isBetaTester,
adminPanel: user.role === "admin",
};Environment-Based Flags
const flags = {
debugMode: import.meta.env.DEV,
newFeature: import.meta.env.VITE_ENABLE_NEW_FEATURE === "true",
};Summary
I implemented a simple feature flag system using React Context.
- Lightweight: No external dependencies, React only
- Type-Safe: Fully typed with TypeScript
- Trunk-Based Development: Safely merge incomplete features
However, client-side feature flags cannot be used for security purposes. For sensitive features, always implement server-side authentication and authorization.
Clone the demo project and try it out.
git clone https://github.com/codenote-net/vite-feature-flag-demo.git
cd vite-feature-flag-demo
pnpm install
pnpm devThat’s all from the Gemba, where I implemented custom feature flags with Vite + React and explored trunk-based development.