ct shape at compile time. TypeScript will throw an error if a component references a non-existent flag, eliminating runtime undefined crashes. Separating types ensures that getBooleanFlag returns { enabled: boolean; loading: boolean } while getStringFlag returns { value: string; loading: boolean }.
Step 2: Build an Environment Resolver
Avoid scattering .env checks across the application. Instead, create a deterministic resolver that selects the correct SDK key based on build-time or runtime context.
// src/utils/environmentResolver.ts
const PRODUCTION_HOSTS = new Set([
'dashboard.production.io',
'app.staging.production.io'
])
export function resolveEnvironment(): 'production' | 'development' {
if (typeof window === 'undefined') {
return process.env.NODE_ENV === 'production' ? 'production' : 'development'
}
return PRODUCTION_HOSTS.has(window.location.hostname)
? 'production'
: 'development'
}
Rationale: ConfigCat SDK keys are public by design and only permit read access. Embedding them in code with a runtime resolver removes the need for environment-specific .env files and prevents accidental leakage of write credentials. The Set lookup provides O(1) performance for hostname matching.
Step 3: Implement the Synchronization Engine
The sync engine bridges the vendor SDK and the React context. It handles user targeting, initial flag fetch, and real-time updates via polling.
// src/providers/flagSyncEngine.tsx
'use client'
import { createContext, useContext, useEffect, useMemo, useState, useCallback } from 'react'
import { ConfigCatProvider, useConfigCatClient, User } from 'configcat-react'
import { resolveEnvironment } from '../utils/environmentResolver'
import { BOOLEAN_FLAGS, STRING_FLAGS, BooleanFlagKey, StringFlagKey } from '../registry/flagDefinitions'
type FlagEntry = { enabled: boolean; loading: boolean }
type StringFlagEntry = { value: string; loading: boolean }
interface FlagContextShape {
getBoolean: (key: BooleanFlagKey) => FlagEntry
getString: (key: StringFlagKey) => StringFlagEntry
isReady: boolean
}
const FlagContext = createContext<FlagContextShape | null>(null)
const SyncEngine = ({ children }: { children: React.ReactNode }) => {
const client = useConfigCatClient()
const [cache, setCache] = useState<Record<string, FlagEntry | StringFlagEntry>>({})
const [ready, setReady] = useState(false)
// Sync user attributes for targeting
useEffect(() => {
if (!client) return
const user = useContext(require('../context/authContext').AuthContext)?.user
if (user) {
client.setDefaultUser(new User(user.id, user.email, undefined, {
tier: user.subscription,
region: user.geo,
internal: user.isStaff ? 'true' : 'false'
}))
} else {
client.clearDefaultUser()
}
}, [client])
// Fetch all flags and listen for remote updates
useEffect(() => {
if (!client) return
const refresh = async () => {
const boolKeys = Object.values(BOOLEAN_FLAGS)
const strKeys = Object.values(STRING_FLAGS)
const boolResults = await Promise.all(
boolKeys.map(async (k) => [k, { enabled: await client.getValueAsync(k, false), loading: false }])
)
const strResults = await Promise.all(
strKeys.map(async (k) => [k, { value: await client.getValueAsync(k, ''), loading: false }])
)
setCache(Object.fromEntries([...boolResults, ...strResults]))
setReady(true)
}
refresh()
client.on('configChanged', refresh)
return () => client.off('configChanged', refresh)
}, [client])
const getBoolean = useCallback((key: BooleanFlagKey) => {
const entry = cache[BOOLEAN_FLAGS[key]]
return entry ?? { enabled: false, loading: true }
}, [cache])
const getString = useCallback((key: StringFlagKey) => {
const entry = cache[STRING_FLAGS[key]]
return entry ?? { value: '', loading: true }
}, [cache])
const value = useMemo(() => ({ getBoolean, getString, isReady: ready }), [getBoolean, getString, ready])
return <FlagContext.Provider value={value}>{children}</FlagContext.Provider>
}
Rationale:
useCallback and useMemo prevent unnecessary re-renders in consuming components.
- Default states return
loading: true and safe fallbacks (false/''), ensuring features remain hidden until the cache populates.
- The
configChanged listener enables real-time updates without page refreshes, leveraging ConfigCat's automatic polling mechanism.
- User targeting is explicitly synchronized before flag evaluation, ensuring percentage rollouts and attribute-based rules resolve correctly.
Step 4: Expose the Provider at the Application Root
Wrap the application with the vendor provider and the custom sync engine. The vendor provider handles SDK initialization and polling; the sync engine handles state distribution.
// src/providers/flagProvider.tsx
'use client'
import { ConfigCatProvider } from 'configcat-react'
import { SyncEngine } from './flagSyncEngine'
import { resolveEnvironment } from '../utils/environmentResolver'
const SDK_KEYS = {
development: 'configcat-sdk-key-dev-12345',
production: 'configcat-sdk-key-prod-67890'
}
export function FlagProvider({ children }: { children: React.ReactNode }) {
const env = resolveEnvironment()
const sdkKey = SDK_KEYS[env]
return (
<ConfigCatProvider sdkKey={sdkKey} options={{ pollIntervalSeconds: 30 }}>
<SyncEngine>{children}</SyncEngine>
</ConfigCatProvider>
)
}
Step 5: Consume Flags in Components
Components interact only with the custom context. They remain completely decoupled from ConfigCat.
// src/components/dashboard/CheckoutFlow.tsx
import { useContext } from 'react'
import { FlagContext } from '../../providers/flagSyncEngine'
export function CheckoutFlow() {
const flags = useContext(FlagContext)
if (!flags) throw new Error('FlagProvider missing in tree')
const { enabled: showBeta, loading: betaLoading } = flags.getBoolean('SHOW_BETA_CHECKOUT')
const { value: promoText, loading: promoLoading } = flags.getString('PROMO_BANNER_TEXT')
if (betaLoading || promoLoading) return <div className="skeleton" />
return (
<section>
{showBeta && <BetaCheckoutUI />}
{promoText && <Banner text={promoText} />}
<LegacyCheckoutUI />
</section>
)
}
Architecture Decisions:
- Decoupling: Swapping ConfigCat for LaunchDarkly or Unleash requires only replacing the vendor provider and adjusting the sync engine. Component trees remain untouched.
- Type Safety: Compile-time flag validation prevents typos and missing keys from reaching production.
- Performance: Cache-level storage avoids repeated SDK calls. Memoized selectors ensure components only re-render when their specific flags change.
- Next.js Compatibility: The
'use client' directive ensures the provider runs in the browser, avoiding SSR hydration mismatches. For App Router, wrap the provider in a client-side boundary component.
Pitfall Guide
1. Scattering SDK Calls Across Components
Explanation: Importing useConfigCatClient directly into UI components creates tight coupling, duplicate network requests, and inconsistent loading states.
Fix: Centralize all SDK interactions inside a single sync engine. Expose flags through a React Context or custom hook.
2. Ignoring Loading States
Explanation: Rendering components before flags resolve causes UI flickering or premature feature exposure.
Fix: Always check loading before rendering conditional UI. Provide skeleton loaders or safe fallbacks. Default to enabled: false until the cache populates.
3. Over-Fetching on Every Render
Explanation: Calling getValueAsync inside component render cycles triggers unnecessary network requests and degrades performance.
Fix: Fetch all flags once at the provider level. Store results in a cache object. Use useMemo and useCallback to stabilize references.
4. Hardcoding Environment Checks
Explanation: Using process.env.NODE_ENV directly in components fails in edge runtimes or custom build pipelines.
Fix: Abstract environment detection into a resolver function that checks build variables and runtime hostnames. Cache the result to avoid repeated evaluations.
5. Neglecting SSR/CSR Boundaries
Explanation: Feature flag SDKs rely on browser APIs and polling. Running them in server components causes hydration errors or stale flag states.
Fix: Wrap flag providers in client-side boundaries. Use 'use client' directives. For Next.js App Router, create a dedicated client wrapper component.
6. Targeting Without User Context
Explanation: Percentage rollouts and attribute-based rules fail if the SDK doesn't know which user is evaluating the flag.
Fix: Explicitly sync user attributes (id, email, tier, region) with the SDK before flag evaluation. Clear user context on logout.
7. Flag Rot and Orphaned Toggles
Explanation: Unused flags accumulate over time, increasing bundle size, complicating testing, and creating maintenance debt.
Fix: Implement a flag lifecycle policy. Tag flags with creation dates and owners. Run automated scans to detect flags unused for >90 days. Remove them during quarterly tech debt sprints.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small team, limited budget | ConfigCat (Free Tier) | Generous limits, intuitive UI, native React SDK | $0/month up to 10k requests |
| Enterprise compliance, audit trails | LaunchDarkly | SOC2, RBAC, detailed analytics, enterprise support | $25k+/year |
| Self-hosted, full data control | Unleash (Open Source) | No vendor lock-in, customizable, runs on existing infra | Infrastructure + maintenance overhead |
| Internal tooling, rapid iteration | Custom in-memory flags | Zero external dependencies, instant updates | High dev overhead, no remote control |
Configuration Template
// src/providers/flagProvider.tsx
'use client'
import { ConfigCatProvider } from 'configcat-react'
import { SyncEngine } from './flagSyncEngine'
import { resolveEnvironment } from '../utils/environmentResolver'
const SDK_KEYS = {
development: process.env.NEXT_PUBLIC_CONFIGCAT_DEV_KEY || 'dev-key-placeholder',
production: process.env.NEXT_PUBLIC_CONFIGCAT_PROD_KEY || 'prod-key-placeholder'
}
export function FlagProvider({ children }: { children: React.ReactNode }) {
const env = resolveEnvironment()
const sdkKey = SDK_KEYS[env]
return (
<ConfigCatProvider
sdkKey={sdkKey}
options={{
pollIntervalSeconds: 30,
cacheTimeToLiveSeconds: 60,
maxInitWaitTime: 5000
}}
>
<SyncEngine>{children}</SyncEngine>
</ConfigCatProvider>
)
}
Quick Start Guide
- Install the SDK: Run
npm install configcat-react and create your ConfigCat account. Generate SDK keys for development and production environments.
- Define Flags: Create a typed registry file. Add boolean and string flag identifiers matching your ConfigCat dashboard keys.
- Build the Provider: Implement the environment resolver, sync engine, and context wrapper. Wrap your application root with the provider.
- Consume in Components: Import the context, call
getBoolean or getString, handle loading states, and render conditional UI. Verify targeting rules in the ConfigCat dashboard.