flipflag-js-sdk
v1.0.0
Published
Lightweight TypeScript SDK for FlipFlag feature flags service. Works in browser and Node.js environments.
Maintainers
Readme
FlipFlag JavaScript SDK
Lightweight TypeScript SDK for FlipFlag feature flags service. Works seamlessly in both browser and Node.js environments with zero external dependencies.
🚀 Features
- 🌐 Universal: Works in both browser and Node.js environments
- ⚡ Lightweight: No external dependencies, only uses native
fetch - 🔄 Auto-updating: Configurable polling for real-time flag updates
- 💾 Smart Caching: Uses localStorage (browser) or in-memory cache (Node.js)
- 📢 Event-driven: Subscribe to flag changes and updates
- 🛡️ Error Resilient: Graceful fallback to cached values when server is unavailable
- 📝 TypeScript: Written in TypeScript with full type definitions
- ⚛️ React Ready: Global hook system for React apps without prop drilling
📦 Installation
npm install flipflag-js-sdkFor Node.js versions < 18, you'll also need to install node-fetch:
npm install node-fetch🏁 Quick Start
React (Recommended for React apps)
// 1. Initialize once in your app root
import React, { useEffect } from "react";
import { initFlipFlag } from "flipflag-js-sdk/react";
function App() {
useEffect(() => {
initFlipFlag({
apiKey: "ff_your_api_key_here",
environment: "prod",
});
}, []);
return (
<div>
<Dashboard />
</div>
);
}
// 2. Use in any component without prop drilling
import { useFlipFlag } from "flipflag-js-sdk/react";
function Dashboard() {
const { isEnabled } = useFlipFlag();
return (
<div>
{isEnabled("new-feature") && <NewFeature />}
{isEnabled("dark-mode") ? <DarkTheme /> : <LightTheme />}
</div>
);
}Browser
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/flipflag-js-sdk@latest/dist/flipflag-sdk.js"></script>
</head>
<body>
<script>
const flipflag = new FlipFlagSDK();
flipflag
.init({
apiKey: "ff_your_api_key_here",
environment: "prod",
})
.then(() => {
console.log("SDK initialized!");
if (flipflag.isEnabled("dark-mode")) {
document.body.classList.add("dark-theme");
}
if (flipflag.isEnabled("new-dashboard")) {
loadNewDashboard();
} else {
loadLegacyDashboard();
}
});
</script>
</body>
</html>Node.js
import FlipFlagSDK from "flipflag-js-sdk";
async function main() {
const flipflag = new FlipFlagSDK();
await flipflag.init({
apiKey: "ff_your_api_key_here",
baseUrl: "https://api.flipflag.dev",
environment: "prod",
pollInterval: 60000, // Poll every 60 seconds
});
// Check feature flags
if (flipflag.isEnabled("new-feature")) {
console.log("New feature is enabled!");
// Implement new feature logic
}
if (flipflag.isEnabled("analytics-tracking")) {
// Initialize analytics
analytics.init();
}
// Subscribe to flag updates
flipflag.on("update", (data) => {
console.log("Flags updated:", data.changedFlags);
});
}
main().catch(console.error);📖 API Reference
Constructor
const flipflag = new FlipFlagSDK();Methods
init(config)
Initialize the SDK with your configuration.
Parameters:
config.apiKey(string, required): Your FlipFlag API keyconfig.projectId(string, optional): Project ID for better performanceconfig.baseUrl(string, optional): API base URL (default:http://localhost:3002)config.environment(string, optional): Environment (dev,staging,prod) (default:prod)config.pollInterval(number, optional): Polling interval in milliseconds (default:60000)
Returns: Promise<void>
await flipflag.init({
apiKey: "ff_your_api_key_here",
baseUrl: "https://api.flipflag.dev",
environment: "prod",
pollInterval: 30000,
});isEnabled(flagName, defaultValue?)
Check if a feature flag is enabled.
Parameters:
flagName(string): Name of the feature flagdefaultValue(boolean, optional): Default value if flag is not found (default:false)
Returns: boolean
const isEnabled = flipflag.isEnabled("dark-mode");
const isEnabledWithDefault = flipflag.isEnabled("new-feature", true);getAllFlags()
Get all flags as an object.
Returns: Record<string, boolean> - All flags as key-value pairs
const allFlags = flipflag.getAllFlags();
console.log(allFlags); // { "dark-mode": true, "new-dashboard": false }refresh()
Manually refresh flags from the server.
Returns: Promise<void>
await flipflag.refresh();on(event, callback)
Subscribe to events.
Events:
'initialized': Fired when SDK is initialized'update': Fired when flags are updated'error': Fired when an error occurs
flipflag.on("update", (data) => {
console.log("Changed flags:", data.changedFlags);
console.log("All flags:", data.flags);
});
flipflag.on("error", (error) => {
console.error("SDK Error:", error.error);
});off(event, callback)
Unsubscribe from events.
const updateHandler = (data) => console.log("Updated:", data);
flipflag.on("update", updateHandler);
flipflag.off("update", updateHandler);destroy()
Stop polling and cleanup resources.
flipflag.destroy();⚛️ React Integration
Global Hook Approach
1. Initialize in App Root
// App.tsx
import React, { useEffect } from "react";
import { initFlipFlag } from "flipflag-js-sdk/react";
const App: React.FC = () => {
useEffect(() => {
initFlipFlag({
apiKey: process.env.REACT_APP_FLIPFLAG_API_KEY!,
projectId: process.env.REACT_APP_FLIPFLAG_PROJECT_ID,
environment: process.env.NODE_ENV === "production" ? "prod" : "dev",
baseUrl: process.env.REACT_APP_FLIPFLAG_BASE_URL,
}).catch(console.error);
}, []);
return (
<div className="App">
<Dashboard />
</div>
);
};
export default App;2. Use Hook in Any Component
// components/Dashboard.tsx
import React from "react";
import { useFlipFlag } from "flipflag-js-sdk/react";
const Dashboard: React.FC = () => {
const { isEnabled, flags, isInitialized } = useFlipFlag();
if (!isInitialized) {
return <div>Loading feature flags...</div>;
}
return (
<div className={isEnabled("dark-mode") ? "dark-theme" : "light-theme"}>
<h1>Dashboard</h1>
{isEnabled("new-dashboard") ? <NewDashboard /> : <LegacyDashboard />}
{isEnabled("analytics-tracking") && <AnalyticsComponent />}
{isEnabled("beta-features") && (
<div className="beta-section">
<h2>Beta Features</h2>
<BetaFeaturesList />
</div>
)}
</div>
);
};
export default Dashboard;3. Use Anywhere Without Prop Drilling
// components/Header.tsx
import React from "react";
import { useFlipFlag } from "flipflag-js-sdk/react";
const Header: React.FC = () => {
const { isEnabled } = useFlipFlag();
return (
<header>
<h1>{isEnabled("new-branding") ? "FlipFlag Pro" : "FlipFlag"}</h1>
{isEnabled("search-feature") && <SearchBar />}
{isEnabled("notifications") && <NotificationBell />}
</header>
);
};
export default Header;Declarative Component Approach
import React from "react";
import { FeatureFlag } from "flipflag-js-sdk/react";
const ProductPage: React.FC = () => {
return (
<div>
<h1>Product Page</h1>
<FeatureFlag flag="new-product-gallery">
<NewProductGallery />
</FeatureFlag>
<FeatureFlag
flag="reviews-section"
fallback={<div>Reviews coming soon!</div>}
>
<ReviewsSection />
</FeatureFlag>
<FeatureFlag flag="chat-support">
<ChatWidget />
</FeatureFlag>
</div>
);
};Higher-Order Component Approach
import React from "react";
import { withFeatureFlag } from "flipflag-js-sdk/react";
// Only render component if flag is enabled
const BetaFeatureComponent: React.FC = () => <div>This is a beta feature!</div>;
const BetaFeature = withFeatureFlag("beta-features")(BetaFeatureComponent);
// Usage
const App: React.FC = () => (
<div>
<h1>My App</h1>
<BetaFeature /> {/* Only renders if 'beta-features' flag is true */}
</div>
);Outside React Components
// utils/analytics.ts
import { isEnabled } from "flipflag-js-sdk/react";
export const trackEvent = (eventName: string, data: any) => {
if (isEnabled("analytics-tracking")) {
// Send analytics event
analytics.track(eventName, data);
}
};
export const getAnalyticsConfig = () => {
return {
enableAdvancedTracking: isEnabled("advanced-analytics"),
enableHeatmaps: isEnabled("heatmap-tracking"),
samplingRate: isEnabled("high-volume-analytics") ? 1.0 : 0.1,
};
};🌐 Next.js Integration
App Router (Next.js 13+)
// app/components/flipflag-initializer.tsx
"use client";
import { useEffect } from "react";
import { initFlipFlag } from "flipflag-js-sdk/react";
export default function FlipFlagInitializer() {
useEffect(() => {
initFlipFlag({
apiKey: process.env.NEXT_PUBLIC_FLIPFLAG_API_KEY!,
projectId: process.env.NEXT_PUBLIC_FLIPFLAG_PROJECT_ID,
environment: process.env.NODE_ENV === "production" ? "prod" : "dev",
baseUrl:
process.env.NEXT_PUBLIC_FLIPFLAG_BASE_URL || "http://localhost:3002",
pollInterval: 60000,
}).catch(console.error);
}, []);
return null; // This component doesn't render anything
}// app/layout.tsx
import FlipFlagInitializer from "./components/flipflag-initializer";
import "./globals.css";
export const metadata = {
title: "My Next.js App with Feature Flags",
description: "App using FlipFlag for feature management",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<FlipFlagInitializer />
{children}
</body>
</html>
);
}// app/components/dashboard.tsx
"use client";
import { useFlipFlag } from "flipflag-js-sdk/react";
export default function Dashboard() {
const { isEnabled, flags, isInitialized } = useFlipFlag();
if (!isInitialized) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading feature flags...</p>
</div>
</div>
);
}
return (
<div className={isEnabled("dark-mode") ? "dark" : ""}>
<div className="min-h-screen bg-white dark:bg-gray-900 text-black dark:text-white">
<header className="p-6 border-b border-gray-200 dark:border-gray-700">
<h1 className="text-3xl font-bold">
{isEnabled("new-branding") ? "FlipFlag Pro Dashboard" : "Dashboard"}
</h1>
</header>
<main className="p-6">
{isEnabled("new-dashboard-layout") ? (
<NewDashboardLayout />
) : (
<LegacyDashboardLayout />
)}
{isEnabled("analytics-widget") && (
<div className="mt-6">
<AnalyticsWidget />
</div>
)}
{isEnabled("beta-features") && (
<div className="mt-6 p-4 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
<h2 className="text-lg font-semibold mb-2">🚀 Beta Features</h2>
<BetaFeaturesPanel />
</div>
)}
</main>
</div>
</div>
);
}Pages Router (Next.js 12 and below)
// pages/_app.tsx
import { useEffect } from "react";
import { initFlipFlag } from "flipflag-js-sdk/react";
import "../styles/globals.css";
import { AppProps } from "next/app";
function MyApp({ Component, pageProps }: AppProps) {
useEffect(() => {
initFlipFlag({
apiKey: process.env.NEXT_PUBLIC_FLIPFLAG_API_KEY!,
projectId: process.env.NEXT_PUBLIC_FLIPFLAG_PROJECT_ID,
environment: process.env.NODE_ENV === "production" ? "prod" : "dev",
baseUrl: process.env.NEXT_PUBLIC_FLIPFLAG_BASE_URL,
}).catch(console.error);
}, []);
return <Component {...pageProps} />;
}
export default MyApp;// pages/index.tsx
import { useFlipFlag } from "flipflag-js-sdk/react";
import { useEffect, useState } from "react";
export default function Home() {
const { isEnabled, isInitialized } = useFlipFlag();
const [mounted, setMounted] = useState(false);
// Prevent hydration mismatch
useEffect(() => {
setMounted(true);
}, []);
if (!mounted || !isInitialized) {
return <div>Loading...</div>;
}
return (
<div className={isEnabled("dark-mode") ? "dark" : ""}>
<main className="min-h-screen bg-white dark:bg-gray-900">
<h1 className="text-4xl font-bold p-8">
Welcome to Next.js with FlipFlag!
</h1>
{isEnabled("hero-banner") && (
<section className="bg-blue-500 text-white p-8 m-4 rounded">
<h2 className="text-2xl font-bold">🚀 New Hero Banner!</h2>
<p>This banner is controlled by the 'hero-banner' feature flag.</p>
</section>
)}
{isEnabled("pricing-section") && <PricingSection />}
{isEnabled("testimonials") && <TestimonialsSection />}
</main>
</div>
);
}Server-Side Feature Flags with API Routes
// pages/api/dashboard-config.ts
import { NextApiRequest, NextApiResponse } from "next";
import FlipFlagSDK from "flipflag-js-sdk";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// Initialize FlipFlag SDK on server-side
const flipflag = new FlipFlagSDK();
try {
await flipflag.init({
apiKey: process.env.FLIPFLAG_API_KEY!, // Server-side env var
projectId: process.env.FLIPFLAG_PROJECT_ID,
environment: process.env.NODE_ENV === "production" ? "prod" : "dev",
baseUrl: process.env.FLIPFLAG_BASE_URL,
pollInterval: 0, // Disable polling on server
});
const config = {
showPremiumFeatures: flipflag.isEnabled("premium-features"),
enableNewAPI: flipflag.isEnabled("new-api-format"),
maintenanceMode: flipflag.isEnabled("maintenance-mode"),
maxUsers: flipflag.isEnabled("increased-limits") ? 10000 : 1000,
};
flipflag.destroy();
res.status(200).json(config);
} catch (error) {
console.error("Failed to fetch feature flags:", error);
// Return default configuration
res.status(200).json({
showPremiumFeatures: false,
enableNewAPI: false,
maintenanceMode: false,
maxUsers: 1000,
});
}
}🔧 Configuration
Environment Variables
# React/Next.js Client-side
REACT_APP_FLIPFLAG_API_KEY=ff_your_client_api_key
REACT_APP_FLIPFLAG_PROJECT_ID=your-project-id
REACT_APP_FLIPFLAG_BASE_URL=https://api.flipflag.dev
# Next.js
NEXT_PUBLIC_FLIPFLAG_API_KEY=ff_your_client_api_key
NEXT_PUBLIC_FLIPFLAG_PROJECT_ID=your-project-id
NEXT_PUBLIC_FLIPFLAG_BASE_URL=https://api.flipflag.dev
# Server-side only (for API routes)
FLIPFLAG_API_KEY=ff_your_server_api_key
FLIPFLAG_PROJECT_ID=your-project-id
FLIPFLAG_BASE_URL=https://api.flipflag.devEnvironment-specific Configuration
// Development
await flipflag.init({
apiKey: "ff_dev_key",
environment: "dev",
pollInterval: 10000, // More frequent polling in dev
});
// Production
await flipflag.init({
apiKey: "ff_prod_key",
environment: "prod",
pollInterval: 300000, // Less frequent polling in prod
});Custom Polling Strategy
// Disable automatic polling and use manual refresh
await flipflag.init({
apiKey: "ff_your_api_key",
pollInterval: 0, // Disable polling
});
// Manually refresh when needed
setInterval(async () => {
try {
await flipflag.refresh();
} catch (error) {
console.warn("Manual refresh failed:", error);
}
}, 120000); // Every 2 minutes💾 Caching
The SDK automatically caches flag values to ensure your application continues to work even when the FlipFlag service is temporarily unavailable.
Browser
- Uses
localStorageto persist flags across browser sessions - Automatically falls back to cached values on network errors
Node.js
- Uses in-memory caching
- Cache is lost when the process restarts
Cache Key Format
flipflag_{apiKey}_{environment}🔄 Polling and Updates
The SDK automatically polls for flag updates at the specified interval. When flags change, the SDK emits update events.
// Set up flag change handlers
flipflag.on("update", (data) => {
// Handle any flag changes
Object.keys(data.changedFlags).forEach((flagName) => {
console.log(`Flag ${flagName} changed to: ${data.changedFlags[flagName]}`);
});
});🛡️ Error Handling
The SDK is designed to be resilient to network issues and API errors.
flipflag.on("error", (error) => {
switch (error.type) {
case "initialization":
console.error("Failed to initialize:", error.error);
break;
case "polling":
console.warn("Polling failed, using cached data:", error.error);
break;
case "network":
console.warn("Network error:", error.error);
break;
case "authentication":
console.error("Authentication error:", error.error);
break;
}
});
try {
await flipflag.init({ apiKey: "invalid_key" });
} catch (error) {
console.error("Initialization failed:", error.message);
// App can still function with default values
const isFeatureEnabled = flipflag.isEnabled("my-feature", false);
}🧪 Testing
When testing your application, you can mock the FlipFlag SDK:
// Mock for Jest
jest.mock("flipflag-js-sdk", () => ({
__esModule: true,
default: class MockFlipFlagSDK {
private flags = new Map<string, boolean>();
async init() {}
isEnabled(flagName: string, defaultValue: boolean = false): boolean {
return this.flags.get(flagName) ?? defaultValue;
}
// Test helper to set flag values
setFlag(flagName: string, value: boolean) {
this.flags.set(flagName, value);
}
on() {}
off() {}
destroy() {}
},
}));
// In your test
import FlipFlagSDK from "flipflag-js-sdk";
const flipflag = new FlipFlagSDK();
(flipflag as any).setFlag("new-feature", true);
expect(flipflag.isEnabled("new-feature")).toBe(true);🚨 Error Codes
| Error | Description | Solution |
| ------------------------------------ | ----------------------------------------- | -------------------------------------------- |
| API key is required | No API key provided | Provide a valid API key in init() |
| Invalid API key format | API key doesn't start with ff_ | Use a valid FlipFlag API key |
| Invalid API key | API key is not valid | Check your API key in FlipFlag dashboard |
| API key not valid for this project | API key belongs to different project | Use the correct API key for your project |
| Too many requests | Rate limit exceeded | Reduce polling frequency or retry later |
| Project not found | Project associated with API key not found | Verify project exists and API key is correct |
📄 License
MIT License - see LICENSE file for details.
🤝 Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📞 Support
- 📚 Documentation
- 🐛 Bug Reports
- 💬 Discussions
- 📧 Email: [email protected]
Made with ❤️ by the FlipFlag team
