UTA DevHub
Guides

Feature Flag Implementation Guide

Patterns for implementing feature flags using a context provider.

Feature Flag Implementation Guide

Overview

This guide outlines the implementation of a feature flag system using a React Context provider. It allows toggling features on/off based on configuration retrieved from a remote source or local defaults, potentially considering user context (like role or ID).

When To Use

Use feature flags to:

  • Gradually roll out new features to specific user segments or percentages.
  • Perform A/B testing by enabling features for different groups.
  • Quickly disable problematic features in production without a full deployment.
  • Manage different feature sets for various environments (development, staging, production).
  • Control access to premium or beta features.

Implementation Patterns

1. Define Flag Types and Context

Establish the structure for flags and the context used for evaluation.

// src/core/featureFlags/types/featureFlags.ts
export interface FeatureFlags {
  // Example flags - Define your actual flags here
  enableNewDashboard: boolean;
  showAdvancedAnalytics: boolean;
  useOptimizedImageLoading: boolean;
  enableOfflineMode: boolean;
  allowMultiAccount: boolean;
}
 
// Context provided to the flag evaluation logic
export interface FlagEvaluationContext {
  user?: {
    id: string;
    email?: string;
    role?: string; // e.g., 'admin', 'beta_tester', 'premium_user'
    groups?: string[];
  };
  device?: {
    platform: 'ios' | 'android' | 'web';
    version: string;
  };
  environment: 'development' | 'staging' | 'production';
  // Add other relevant context, like region, app version etc.
}

2. Feature Flag Provider

A context provider that fetches flag configurations and makes them available.

// src/core/featureFlags/context/FeatureFlagContext.tsx
import React, { createContext, useState, useEffect, useContext, useMemo, useCallback } from 'react';
import type { FeatureFlags, FlagEvaluationContext } from '../types/featureFlags';
import { featureFlagService } from '../services/featureFlagService'; // Service to fetch flags
import { useAuth } from '@/features/auth/hooks/useAuth'; // To get user context
import { Platform } from 'react-native';
import Constants from 'expo-constants'; // For environment/app info
 
interface FeatureFlagContextType {
  flags: FeatureFlags;
  isLoading: boolean;
  error: Error | null;
  refreshFlags: () => Promise<void>;
  getFlag: (flagName: keyof FeatureFlags, defaultValue?: boolean) => boolean;
  trackExposure: (flagName: keyof FeatureFlags) => void; // For analytics
}
 
// Define default flags (important fallback)
const defaultFlags: FeatureFlags = {
  enableNewDashboard: false,
  showAdvancedAnalytics: false,
  useOptimizedImageLoading: true,
  enableOfflineMode: false,
  allowMultiAccount: true,
};
 
const FeatureFlagContext = createContext<FeatureFlagContextType>({
  flags: defaultFlags,
  isLoading: true,
  error: null,
  refreshFlags: async () => {},
  getFlag: (flagName, defaultValue) => defaultFlags[flagName] ?? defaultValue ?? false,
  trackExposure: () => {},
});
 
export const FeatureFlagProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [flags, setFlags] = useState<FeatureFlags>(defaultFlags);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const { user } = useAuth(); // Get user from auth context
 
  // Memoize the evaluation context to avoid unnecessary fetches
  const evaluationContext = useMemo((): FlagEvaluationContext => ({
    user: user ? {
        id: user.id,
        email: user.email,
        role: user.role,
        // groups: user.groups, // Add groups if applicable
    } : undefined,
    device: {
        platform: Platform.OS,
        version: Platform.Version.toString(),
    },
    environment: Constants.expoConfig?.extra?.environment ?? 'development',
  }), [user]);
 
  // Function to fetch flags
  const fetchFlags = useCallback(async () => {
    setIsLoading(true);
    setError(null);
    try {
      console.log('[FeatureFlagProvider] Fetching flags with context:', evaluationContext);
      const fetchedFlags = await featureFlagService.getFlags(evaluationContext);
      // Merge fetched flags with defaults to ensure all flags are present
      setFlags({ ...defaultFlags, ...fetchedFlags });
      console.log('[FeatureFlagProvider] Flags updated:', { ...defaultFlags, ...fetchedFlags });
    } catch (err: any) {
      console.error('[FeatureFlagProvider] Error fetching feature flags:', err);
      setError(err);
      setFlags(defaultFlags); // Fallback to defaults on error
    } finally {
      setIsLoading(false);
    }
  }, [evaluationContext]); // Dependency: evaluationContext
 
  // Fetch flags on mount and when context changes (e.g., user logs in/out)
  useEffect(() => {
    fetchFlags();
  }, [fetchFlags]);
 
  // Function to get a specific flag with a fallback default value
  const getFlag = useCallback((flagName: keyof FeatureFlags, defaultValue: boolean = false): boolean => {
      return flags[flagName] ?? defaultValue;
  }, [flags]);
 
  // Placeholder for exposure tracking (send to analytics)
  const trackExposure = useCallback((flagName: keyof FeatureFlags) => {
      // console.log(`[FeatureFlagProvider] Exposure event for flag: ${flagName}, value: ${flags[flagName]}`);
      // Implement analytics tracking call here (e.g., Segment, Mixpanel)
  }, [/* flags */]); // Track only when flag value changes if needed
 
  const value = useMemo(() => ({
      flags,
      isLoading,
      error,
      refreshFlags: fetchFlags, // Expose refresh function
      getFlag,
      trackExposure,
  }), [flags, isLoading, error, fetchFlags, getFlag, trackExposure]);
 
  return (
    <FeatureFlagContext.Provider value={value}>
      {children}
    </FeatureFlagContext.Provider>
  );
};
 
// Hook to use the feature flags
export const useFeatureFlags = () => useContext(FeatureFlagContext);
 
// Hook to get a single flag value (recommended usage)
export const useFeatureFlag = (flagName: keyof FeatureFlags, defaultValue: boolean = false): boolean => {
    const { getFlag, trackExposure, isLoading } = useFeatureFlags();
    const flagValue = getFlag(flagName, defaultValue);
 
    // Track exposure when the component mounts and potentially when value changes
    useEffect(() => {
        // Optionally check isLoading to avoid tracking default value during init
        // if (!isLoading) {
             trackExposure(flagName);
        // }
    }, [flagName, flagValue, trackExposure, isLoading]);
 
    return flagValue;
};

3. Feature Flag Service

Abstracts the logic for fetching flag configurations (e.g., from a backend or a service like LaunchDarkly, Firebase Remote Config).

// src/core/featureFlags/services/featureFlagService.ts
import type { FeatureFlags, FlagEvaluationContext } from '../types/featureFlags';
import { apiClient } from '@/core/api/apiClient'; // Use your standard API client
 
// Placeholder endpoint - replace with your actual backend endpoint
const FEATURE_FLAG_ENDPOINT = '/feature-flags'; 
 
export const featureFlagService = {
  /**
   * Fetches feature flags based on the provided context.
   * Replace this with your actual implementation (e.g., calling LaunchDarkly, Firebase Remote Config, or your backend).
   */
  getFlags: async (context: FlagEvaluationContext): Promise<Partial<FeatureFlags>> => {
    console.log('[FeatureFlagService] Requesting flags for context:', context);
    
    // --- Example: Fetching from a custom backend endpoint ---
    try {
        const response = await apiClient.post<Partial<FeatureFlags>>(FEATURE_FLAG_ENDPOINT, context);
        console.log('[FeatureFlagService] Received flags from backend:', response.data);
        return response.data;
    } catch (error) {
        console.error('[FeatureFlagService] Failed to fetch flags from backend:', error);
        // Fallback to empty object or throw error depending on desired behavior
        return {}; 
    }
 
    // --- Example: Using a placeholder for local development ---
    // await new Promise(resolve => setTimeout(resolve, 300)); // Simulate network delay
    // return { 
    //     enableNewDashboard: context.user?.role === 'admin' || context.user?.email?.endsWith('@yourcompany.com'),
    //     showAdvancedAnalytics: context.user?.role === 'admin' 
    // };
  },
};

4. Using Feature Flags in Components

Conditionally render UI or enable functionality based on flag values.

// src/features/dashboard/screens/DashboardScreen.tsx
import React from 'react';
import { View, Text } from 'react-native';
import { useFeatureFlag, useFeatureFlags } from '@/core/featureFlags/context/FeatureFlagContext';
import { OldDashboard } from '../components/OldDashboard';
import { NewDashboard } from '../components/NewDashboard';
import { AnalyticsSection } from '../components/AnalyticsSection';
import LoadingSpinner from '@/ui/components/LoadingSpinner'; // Example loading component
 
export const DashboardScreen = () => {
  // Use the hook to get flag values
  const enableNewDashboard = useFeatureFlag('enableNewDashboard');
  const showAdvancedAnalytics = useFeatureFlag('showAdvancedAnalytics', false); // Provide default
 
  // Optionally get loading state if needed for initial render
  const { isLoading: flagsLoading } = useFeatureFlags();
 
  if (flagsLoading) {
      return <LoadingSpinner text="Loading configuration..." />;
  }
 
  return (
    <View>
      <Text>Dashboard</Text>
      
      {/* Conditional rendering based on feature flag */}
      {enableNewDashboard ? <NewDashboard /> : <OldDashboard />}
 
      {/* Conditionally render another section */}
      {showAdvancedAnalytics && <AnalyticsSection />}
    </View>
  );
};

5. Integration with Application Setup

Wrap your application with the FeatureFlagProvider.

// src/App.tsx (or your main entry point)
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { QueryClientProvider } from '@tanstack/react-query';
import { Provider as StoreProvider } from 'react-redux'; // Redux Provider
import { PersistGate } from 'redux-persist/integration/react'; // Redux Persist
 
import { AppNavigator } from '@/navigation/AppNavigator';
import { AuthProvider } from '@/core/auth/providers/AuthProvider';
import { FeatureFlagProvider } from '@/core/featureFlags/context/FeatureFlagContext';
import { queryClient } from '@/core/query/queryClient';
import { store, persistor } from '@/core/store';
import LoadingScreen from '@/features/core/screens/LoadingScreen'; // Your global loading screen
 
// Initialize API interceptors etc. here if needed
 
const App = () => {
  return (
    <SafeAreaProvider>
      <StoreProvider store={store}>
        <PersistGate loading={<LoadingScreen />} persistor={persistor}>
          <QueryClientProvider client={queryClient}>
            <AuthProvider>
              {/* FeatureFlagProvider is often placed inside AuthProvider 
                  if flags depend on user context */} 
              <FeatureFlagProvider>
                <AppNavigator />
              </FeatureFlagProvider>
            </AuthProvider>
          </QueryClientProvider>
        </PersistGate>
      </StoreProvider>
    </SafeAreaProvider>
  );
};
 
export default App;

Common Challenges

  • Loading State: Feature flags might not be available immediately on app start. UI components should handle a loading state or use default flag values until the provider fetches the configuration.
  • Context Dependency: If flags depend on user attributes (role, ID), ensure the FeatureFlagProvider is placed after the AuthProvider in the component tree so user data is available.
  • Caching: Decide on a caching strategy for flags. Fetching them on every app start might be slow. Consider caching results in AsyncStorage with a TTL (Time To Live) or relying on the caching mechanisms of third-party services (like LaunchDarkly).
  • Testing: Mocking feature flags in unit and E2E tests is crucial. Provide a way to override flag values in test environments.
  • Stale Flags: If flags change remotely, the app needs a mechanism to refresh them (e.g., periodic background refresh, refresh on resume, manual refresh trigger). The refreshFlags function exposed by the hook can be used for this.

Performance Considerations

  • Fetch Time: Fetching flags from a remote service adds to app startup time. Optimize the fetching process and use defaults/caching.
  • Context Updates: If the evaluationContext (especially user data) changes frequently, it could trigger unnecessary flag re-fetches. Use useMemo for the context object as shown in the provider example.
  • useFeatureFlags vs useFeatureFlag: Using useFeatureFlags in many components can cause them all to re-render if any flag changes. Prefer using the specific useFeatureFlag hook in components, as it typically only re-renders if the specific flag it consumes changes (depending on context implementation).

Examples

A/B Testing Example

const useOptimizedLoading = useFeatureFlag('useOptimizedImageLoading');
 
return (
    <ImageComponent 
        source={source}
        // Pass flag to component or use different components
        useOptimizedLoading={useOptimizedLoading} 
    />
);

Gradual Rollout by User ID

// featureFlagService.ts (Conceptual - logic inside your service/backend)
function isUserInRollout(userId: string, percentage: number): boolean {
    // Simple consistent hash based rollout (example)
    const hash = simpleHash(userId); // Implement a simple hashing function
    return (hash % 100) < percentage;
}
 
// ... inside getFlags ...
// return {
//     enableNewFeature: isUserInRollout(context.user.id, 10) // 10% rollout
// };

Disabling a Feature Quickly

Simply toggle the flag (e.g., enableProblematicFeature) to false in your feature flag management service (LaunchDarkly, Firebase Remote Config, your backend). The app will pick up the change on the next fetch/refresh.

  • DOC-01: Core Architecture Reference
  • See Also: Authentication Flow Implementation Guide (for user context)