UNPKG

eehitus-feature-flag-client

Version:

A feature flag client for Node.js and React

422 lines (326 loc) 12.1 kB
# Feature Flag Client A universal **feature flag client** that works for both **Node.js** and **React** applications. This package allows applications to fetch feature flags from a remote API and determine if a feature is enabled or disabled. ## Features - ✅ Works in both **Node.js (CommonJS & ESM)** and **React (browser environment)** - ✅ Fetches feature flags from a configurable API - ✅ Uses the optimized `/flags/active` endpoint with sub-10ms response time - ✅ Supports **automatic polling** every minute (for Node.js) - ✅ **NEW:** Supports **configurable polling** for browser environments - ✅ Supports **manual fetching** (for React) - ✅ Uses **Axios** (peer dependency) for HTTP requests --- ## Installation ### **Step 1: Install the package** ```sh # Using Yarn yarn add eehitus-feature-flag-client axios # Using npm npm install eehitus-feature-flag-client axios ``` > **Note:** `axios` is a **peer dependency**, so it must be installed separately. --- ## Usage ### **Node.js (ES Modules)** ```javascript import { FeatureFlagClient } from "eehitus-feature-flag-client"; const client = new FeatureFlagClient( "https://your-feature-flag-api.com", // API URL (without /flags/active) 1234, // Application ID "live" // Environment (dev, test, prelive, live) ); // Fetch flags manually (optional, useful for immediate use) await client.fetchOnce(); // Check if a feature is enabled console.log("New Dashboard Enabled:", client.flagEnabled("new-dashboard")); // Check feature flags dynamically every minute setInterval(() => { console.log("Dark Mode Enabled:", client.flagEnabled("dark-mode")); }, 60000); ``` ### **Node.js (CommonJS)** ```javascript const { FeatureFlagClient } = require("eehitus-feature-flag-client"); const client = new FeatureFlagClient( "https://your-feature-flag-api.com", 1234, "production" ); setTimeout(() => { console.log("Feature Enabled:", client.flagEnabled("beta-feature")); }, 70000); ``` ### **React (with Hooks)** ```javascript import { useEffect, useState } from "react"; import { FeatureFlagClient } from "eehitus-feature-flag-client"; // Creating a client with browser polling enabled const client = new FeatureFlagClient( "https://your-feature-flag-api.com", 1234, "dev", { enableBrowserPolling: true, pollingInterval: 2 * 60 * 1000 // Poll every 2 minutes } ); export default function FeatureFlagsExample() { const [flagEnabled, setFlagEnabled] = useState(false); useEffect(() => { async function fetchFlags() { // IMPORTANT: Always await fetchOnce before checking flags await client.fetchOnce(); setFlagEnabled(client.flagEnabled("new-dashboard")); } fetchFlags(); // Clean up when component unmounts return () => { client.stopFetching(); }; }, []); return <div>{flagEnabled ? "Feature is ON" : "Feature is OFF"}</div>; } ``` --- ## Advanced React Implementation For more robust feature flag management in React applications, we recommend using a context-based provider pattern. This approach makes feature flags available throughout your application and handles loading/caching efficiently. ### **Step 1: Create a Feature Flag Provider** Create a file named `FeatureFlagProvider.tsx`: ```tsx import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { FeatureFlagClient } from 'eehitus-feature-flag-client'; // Create a Feature Flag Client instance with browser polling enabled const featureFlagClient = new FeatureFlagClient( process.env.REACT_APP_FLAG_API_URL || 'http://localhost:3000', Number(process.env.REACT_APP_FLAG_APP_ID) || 9, process.env.REACT_APP_FLAG_ENVIRONMENT || 'dev', { enableBrowserPolling: true, pollingInterval: 5 * 60 * 1000 // 5 minutes } ); // Define the context type interface FeatureFlagContextType { isLoading: boolean; flags: Record<string, boolean>; refreshFlags: () => Promise<void>; isFlagEnabled: (flagName: string) => boolean; } // Create context with default values const FeatureFlagContext = createContext<FeatureFlagContextType>({ isLoading: true, flags: {}, refreshFlags: async () => {}, isFlagEnabled: () => false, }); // Create a hook for using feature flags export const useFeatureFlags = () => useContext(FeatureFlagContext); interface FeatureFlagProviderProps { children: ReactNode; } export function FeatureFlagProvider({ children }: FeatureFlagProviderProps): JSX.Element { const [isLoading, setIsLoading] = useState(true); const [flags, setFlags] = useState<Record<string, boolean>>({}); const [error, setError] = useState<Error | null>(null); // Common flags to check const commonFlags = [ 'new-dashboard', 'dark-mode', 'beta-features', // Add any other flags you want to check ]; // Function to load feature flags const loadFeatureFlags = async () => { try { setIsLoading(true); // Use fetchFresh to ensure a new request is made await featureFlagClient.fetchFresh(); // Create a map of all flags for easy access const flagsMap: Record<string, boolean> = {}; commonFlags.forEach(flagName => { flagsMap[flagName] = featureFlagClient.flagEnabled(flagName); }); setFlags(flagsMap); setError(null); } catch (err) { console.error('Failed to load feature flags:', err); setError(err instanceof Error ? err : new Error(String(err))); } finally { setIsLoading(false); } }; // Load flags on component mount useEffect(() => { // Initial load loadFeatureFlags(); // Clean up when component unmounts // The client handles polling automatically return () => { featureFlagClient.stopFetching(); }; }, []); // Function to check if a flag is enabled const isFlagEnabled = (flagName: string): boolean => { // First check our cached flags if (flagName in flags) { return flags[flagName]; } // If not in our cache, check directly from the client return featureFlagClient.flagEnabled(flagName); }; // Function to manually refresh flags const refreshFlags = async (): Promise<void> => { await loadFeatureFlags(); }; return ( <FeatureFlagContext.Provider value={{ isLoading, flags, refreshFlags, isFlagEnabled, }} > {children} </FeatureFlagContext.Provider> ); } // Higher-order component for feature flag conditional rendering interface FeatureFlaggedProps { flagName: string; fallback?: React.ReactNode; children: React.ReactNode; } export function FeatureFlagged({ flagName, fallback = null, children }: FeatureFlaggedProps): JSX.Element { const { isFlagEnabled } = useFeatureFlags(); return isFlagEnabled(flagName) ? <>{children}</> : <>{fallback}</>; } ``` ### **Step 2: Wrap Your Application with the Provider** In your main `index.tsx` or `App.tsx`: ```tsx import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import { FeatureFlagProvider } from './feature-flags/FeatureFlagProvider'; ReactDOM.render( <React.StrictMode> <FeatureFlagProvider> <App /> </FeatureFlagProvider> </React.StrictMode>, document.getElementById('root') ); ``` ### **Step 3: Use Feature Flags in Components** Now you can use feature flags throughout your application in three ways: **Method 1: Using the `useFeatureFlags` hook** ```tsx import { useFeatureFlags } from './feature-flags/FeatureFlagProvider'; function MyComponent() { const { isFlagEnabled, isLoading } = useFeatureFlags(); if (isLoading) { return <div>Loading...</div>; } return ( <div> {isFlagEnabled('dark-mode') && ( <button>Switch to Light Mode</button> )} </div> ); } ``` **Method 2: Using the `FeatureFlagged` component** ```tsx import { FeatureFlagged } from './feature-flags/FeatureFlagProvider'; import OldDashboard from './components/OldDashboard'; import NewDashboard from './components/NewDashboard'; function DashboardPage() { return ( <FeatureFlagged flagName="new-dashboard" fallback={<OldDashboard />} > <NewDashboard /> </FeatureFlagged> ); } ``` **Method 3: Refreshing flags manually** ```tsx import { useFeatureFlags } from './feature-flags/FeatureFlagProvider'; function SettingsPage() { const { refreshFlags, flags } = useFeatureFlags(); return ( <div> <h1>Application Settings</h1> <button onClick={refreshFlags}>Refresh Feature Flags</button> <pre>{JSON.stringify(flags, null, 2)}</pre> </div> ); } ``` --- ## API ### `new FeatureFlagClient(url: string, appId: number, environment: string, options?: FeatureFlagClientOptions)` Creates a new feature flag client instance. | Parameter | Type | Description | |--------------|---------|------------------------------------| | `url` | `string` | Base API endpoint (without `/flags/active`) | | `appId` | `number` | Application identifier (ID) | | `environment` | `string` | Environment (`dev`, `test`, `prelive`, `live`) | | `options` | `object` | Optional configuration options (see below) | #### FeatureFlagClientOptions | Option | Type | Default | Description | |----------------------|-----------|---------|-------------------------------------------| | `enableBrowserPolling` | `boolean` | `false` | Enable polling in browser environments | | `pollingInterval` | `number` | `60000` | Polling interval in milliseconds (default: 1 minute) | ### `.fetchOnce(): Promise<Record<string, boolean>>` Fetches flags if they haven't been loaded before. Returns the current flag states. ### `.fetchFresh(): Promise<Record<string, boolean>>` Forces a fresh fetch of feature flags from the server, regardless of whether they've been loaded before. Returns the updated flag states. ### `.flagEnabled(flagName: string): boolean` Returns `true` if the feature flag is enabled, otherwise `false`. ### `.getFlagEnabledAsync(flagName: string): Promise<boolean>` Asynchronously ensures flags are loaded before checking if a flag is enabled. Useful for initial page load. ### `.ensureFlagsLoaded(): Promise<void>` Ensures that flags have been loaded at least once before proceeding. ### `.startPolling(interval?: number): void` Starts periodic polling for flag updates. If an interval is provided, it updates the polling interval. ### `.stopFetching(): void` Stops automatic flag updates for both Node.js and browser environments. ### `.isPollingActive(): boolean` Checks if polling is currently active. ### `.setPollingInterval(interval: number): void` Updates the polling interval. If polling is already active, it restarts with the new interval. --- ## Important React Notes - **Browser Polling**: The new `enableBrowserPolling` option allows automatic flag refreshing in browser environments. - **Always await fetchOnce() or fetchFresh()**: When using the client directly in React, always await the fetch methods before checking flags to prevent race conditions. - **Context API**: Using the context-based provider approach is recommended for most React applications as it manages loading states and provides a centralized way to access flags. - **Environment Variables**: Configure your application with the appropriate environment variables for your feature flag API. --- ## Cleanup To stop fetching when shutting down your app: ### Node.js ```javascript process.on("SIGINT", () => { console.log("Stopping feature flag updates..."); client.stopFetching(); process.exit(); }); ``` ### React ```javascript useEffect(() => { // Initialize flags... return () => { client.stopFetching(); }; }, []); ``` --- ## License MIT License --- Now, your **Node.js** and **React** applications can seamlessly use feature flags to dynamically enable or disable features! 🚀