eehitus-feature-flag-client
Version:
A feature flag client for Node.js and React
422 lines (326 loc) • 12.1 kB
Markdown
# 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! 🚀