eehitus-feature-flag-client
v1.6.3
Published
A feature flag client for Node.js and React
Maintainers
Readme
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)
- ✅ NEW: Built-in React Provider, Context, and Hooks for easy integration
- ✅ NEW: Conditional exports - import core client or React-specific components
- ✅ NEW: Compatible with updated API format with cache-busting support
- ✅ Fetches feature flags from a configurable API with sub-10ms response time
- ✅ Enhanced: Handles new wrapped response format
{flags: [], _meta: {}} - ✅ NEW: Built-in cache-busting with unique request IDs and metadata
- ✅ 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
- ✅ NEW: CloudFlare CDN compatible with proper headers
Installation
Step 1: Install the package
# Using Yarn
yarn add eehitus-feature-flag-client axios
# Using npm
npm install eehitus-feature-flag-client axiosNote:
axiosis a peer dependency, so it must be installed separately.
API Response Format
The Feature Flag API has been updated to include cache-busting mechanisms and enhanced metadata:
New Response Format:
{
"flags": [
{
"id": 1,
"name": "new-dashboard",
"explanation": "New dashboard interface"
},
{
"id": 2,
"name": "dark-mode",
"explanation": "Dark mode theme support"
}
],
"_meta": {
"requestId": "1755800710303-wr4i346c4cc",
"timestamp": 1755800710315,
"method": "GET",
"cacheBust": {
"timestamp": 1755800710303,
"random": "wr4i346c4cc",
"version": "1.0",
"nonce": "mel123",
"method": "GET"
},
"responseTime": "5.23ms",
"uniqueUrl": "/flags/active?app=9&environment=test×tamp=1755800710303"
}
}Key Features:
- flags: Array of active feature flags for your application
- _meta: Metadata object containing cache-busting information and request details
- requestId: Unique identifier for each request for debugging
- cacheBust: Cache-busting parameters to ensure fresh responses
- responseTime: Server-side processing time for performance monitoring
Import Options
Core Client (Any Environment)
import { FeatureFlagClient } from 'eehitus-feature-flag-client';React Components (React Only)
import {
FeatureFlagProvider,
useFeatureFlags,
FeatureFlagged
} from 'eehitus-feature-flag-client/react';Usage
Node.js (ES Modules)
import { FeatureFlagClient } from "eehitus-feature-flag-client";
const client = new FeatureFlagClient(
"https://your-feature-flag-api.com", // API URL (without /flags/active)
9, // Application ID (use correct app ID)
"test" // 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)
const { FeatureFlagClient } = require("eehitus-feature-flag-client");
const client = new FeatureFlagClient(
"https://your-feature-flag-api.com",
9,
"production"
);
// Flags are automatically fetched and kept up to date
setTimeout(() => {
console.log("Feature Enabled:", client.flagEnabled("beta-feature"));
}, 1000); // Small delay to allow initial fetchReact (Simple Usage)
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",
9,
"dev",
{
enableBrowserPolling: true,
pollingInterval: 2 * 60 * 1000, // Poll every 2 minutes
timeout: 10000 // 10 second timeout
}
);
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>;
}React (Recommended - Using Built-in Provider)
import { FeatureFlagProvider, useFeatureFlags, FeatureFlagged } from 'eehitus-feature-flag-client/react';
// In your app root
function App() {
return (
<FeatureFlagProvider
url="https://your-feature-flag-api.com"
appId={9}
environment="dev"
commonFlags={['new-dashboard', 'dark-mode']}
options={{
enableBrowserPolling: true,
pollingInterval: 2 * 60 * 1000
}}
>
<MyComponent />
</FeatureFlagProvider>
);
}
// In your components
function MyComponent() {
const { isFlagEnabled, isLoading } = useFeatureFlags();
if (isLoading) return <div>Loading...</div>;
return (
<div>
{isFlagEnabled('new-dashboard') && <div>New Dashboard is enabled!</div>}
<FeatureFlagged name="dark-mode" fallback={<div>Dark mode is disabled</div>}>
<div>Dark mode is enabled!</div>
</FeatureFlagged>
</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:
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 || 'https://your-api.com',
Number(process.env.REACT_APP_FLAG_APP_ID) || 9,
process.env.REACT_APP_FLAG_ENVIRONMENT || 'dev',
{
enableBrowserPolling: true,
pollingInterval: 5 * 60 * 1000, // 5 minutes
timeout: 10000
}
);
// 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
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:
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
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
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
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) - Use correct app ID (e.g., 9) |
| 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) |
| timeout | number | 10000 | Request timeout in milliseconds (default: 10 seconds) |
| headers | object | {} | Additional headers to send with requests |
.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.
Error Handling
The client handles the new API error format that includes metadata:
Error Response Format:
{
"error": "Application ID is required",
"timestamp": 1755800321779,
"cacheBust": {
"timestamp": 1755800321779,
"random": "cuhtuekkpm",
"version": "1.0",
"nonce": "melq6ewj",
"method": "GET"
},
"_meta": {
"timestamp": 1755800321779,
"requestId": "1755800321779-3qztnw2os62",
"cacheBypassed": true,
"generated": "2025-08-21T18:18:41.779Z"
}
}Error Handling in Code:
try {
const flags = await client.fetchFresh();
console.log("Flags loaded:", flags);
} catch (error) {
if (error.response?.data?.error) {
console.error("API Error:", error.response.data.error);
console.error("Request ID:", error.response.data._meta?.requestId);
} else {
console.error("Network Error:", error.message);
}
}Cache-Busting Features
The client automatically includes cache-busting parameters to ensure fresh responses:
Automatic Cache-Busting:
- Timestamp: Current timestamp added to requests
- Random ID: Unique random string for each request
- Request ID: Server generates unique ID for tracking
- CloudFlare Compatible: Works with CloudFlare CDN caching
Cache-Busting Parameters:
// Automatically added to requests:
{
app: 9,
environment: "test",
timestamp: 1755800710303,
r: "wr4i346c4cc"
}Important React Notes
- Browser Polling: The new
enableBrowserPollingoption 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.
- App ID: Make sure to use the correct App ID (e.g., 9) that has feature flags configured in your target environment.
Troubleshooting
Common Issues:
No flags returned (empty object)
// Problem: Using wrong App ID
const client = new FeatureFlagClient(url, 2, 'test'); // App ID 2 might have no flags
// Solution: Use correct App ID with configured flags
const client = new FeatureFlagClient(url, 9, 'test'); // App ID 9 has flagsAPI Parameter Errors
// ✅ Correct: Use 'app' parameter
const client = new FeatureFlagClient(url, appId, environment);
// ❌ Wrong: 'applicationId' parameter is not supported
// The API expects 'app' parameter, not 'applicationId'Response Format Issues
If you see errors like TypeError: response.data.forEach is not a function, ensure you're using the latest version of the client that supports the new API format.
Cache Issues
The new API includes cache-busting mechanisms. If you're seeing stale data:
- Use
fetchFresh()to force a new request - Check that cache-busting parameters are included in requests
- Verify CloudFlare cache settings if applicable
Cleanup
To stop fetching when shutting down your app:
Node.js
process.on("SIGINT", () => {
console.log("Stopping feature flag updates...");
client.stopFetching();
process.exit();
});React
useEffect(() => {
// Initialize flags...
return () => {
client.stopFetching();
};
}, []);Version History
v1.6.1 (Latest)
- ✅ MAJOR: Updated to support new API response format
{flags: [], _meta: {}} - ✅ NEW: Added cache-busting support with unique request IDs
- ✅ NEW: Enhanced error handling for new error format
- ✅ NEW: CloudFlare CDN compatibility
- ✅ NEW: Added timeout configuration option
- ✅ FIXED: Resolved
TypeError: forEach is not a functionwith new API format - ✅ IMPROVED: Better TypeScript support with updated interfaces
v1.5.0
- ✅ Added browser polling support
- ✅ Enhanced React integration
- ✅ Improved error handling
License
MIT License
Now, your Node.js and React applications can seamlessly use feature flags with the latest API format including cache-busting and enhanced metadata! 🚀
