Implementing Custom Feature Flags with Vite + React

Tadashi Shigeoka ·  Wed, February 4, 2026

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.

Benefits of Feature Flags

BenefitDescription
Continuous IntegrationSafely merge incomplete features to the main branch
Gradual RolloutsRelease features to a subset of users
A/B TestingTest different implementations with real users
Instant RollbackDisable problematic features without redeployment
Trunk-Based DevelopmentEliminate long-lived feature branches

Trunk-Based Development Flow

A comparison between traditional development flow and trunk-based development with feature flags.

AspectTraditional FlowTrunk-Based Development
Branch StrategyLong-lived feature branchesMerge directly to main branch
PR SizeLarge PRs (weeks of changes)Small PRs (frequent merges)
Merge FrequencyOnce when feature is completeDaily to every few days
RiskHigh conflict risk at merge timeLow conflict, always deployable
Feature ReleaseMerge = ReleaseRelease 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?

AspectCustom ImplementationThird-Party Service
CostFree$50-500+/month
ComplexitySimpleComplex SDK integration
ControlFull ownershipVendor dependency
FeaturesBasic (extensible)Advanced (targeting, analytics)
LatencyNone (client-side)Network request required
Data LocationYour infrastructureMay 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.

TechnologyVersion
Vite7.3.1
React19.2.4
TypeScript5.9.3
TanStack Router1.158.0
Turborepo2.8.3
Biome2.3.14
pnpm10.9.0
Node.js24.13.0

Project Structure

A monorepo structure using pnpm workspaces and Turborepo.

apps/web - Main React App

FileDescription
src/routes/__root.tsxRoot layout (FeatureFlagProvider placement)
src/routes/index.tsxHome page (demo UI)
src/main.tsxEntry point
vite.config.tsVite configuration

packages/feature-flags - Feature Flag Library

FileDescription
src/index.tsPublic API exports
src/types.tsTypeScript type definitions
src/FeatureFlagContext.tsReact Context definition
src/FeatureFlagProvider.tsxProvider component
src/useFeatureFlag.tsCustom React Hooks

Root Configuration Files

FileDescription
turbo.jsonTurborepo pipeline configuration
pnpm-workspace.yamlpnpm workspace definition

Two Patterns

This project implements feature flags using two distinct patterns.

PatternResolved AtUse Case
React Context (Runtime)RuntimeA/B testing, user targeting, dynamic toggles
Environment Variables (Build-time)Build timePer-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=false
import { isEnvFlagEnabled } from "@demo/feature-flags";
 
if (isEnvFlagEnabled("maintenanceMode")) {
  return <MaintenancePage />;
}

Environment variable names are automatically converted from SCREAMING_SNAKE_CASE to camelCase.

Environment VariableParsed Flag Name
VITE_FF_NEW_DASHBOARDnewDashboard
VITE_FF_DARK_MODEdarkMode

Choosing the Right Pattern

AspectReact ContextEnvironment Variables
Change without rebuildYesNo
User-specific targetingYesNo
Per-environment configComplexNative support
Bundle size impactMinimalNone (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.

ConsiderationDescription
All code is included in the bundleEven when a flag is OFF, protected component code is included in the JavaScript bundle
Flags can be bypassedUsers 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

LevelMethodProtects AgainstUse Case
1. UI Toggle (this demo)Conditional renderingCasual usersUX experiments, gradual rollouts
2. Lazy LoadingReact.lazy() + dynamic importLight code inspectionSlightly better, but URL still accessible
3. Server-Side AuthAPI authorization checksUnauthorized data accessRequired for sensitive data
4. Build-Time ExclusionEnvironment variables at buildCode exposureSeparate builds per environment
LayerRoleNotes
Client-SideUI display control only (UX purposes)Not for security. Consider all client code as public information
Server-SideAuthentication, authorization, data filteringVerify 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 dev

That’s all from the Gemba, where I implemented custom feature flags with Vite + React and explored trunk-based development.

References