React Custom Implementation
While Basestack provides official React SDKs (@basestack/flags-react), sometimes you need full control over the implementation. This guide shows you how to build your own custom React integration using the JavaScript SDK (@basestack/flags-js), giving you complete control over the code, hooks, and provider implementation.
When to Use This Guide
Use this custom implementation approach when you need:
- Full Control: Customize hooks, providers, and state management to fit your exact needs
- Custom Logic: Add project-specific features like analytics tracking, custom caching, or error handling
- Framework Integration: Integrate deeply with your existing state management or architecture patterns
- Learning: Understand how feature flags work under the hood in React applications
- Minimal Dependencies: Use only the core JavaScript SDK without additional React-specific packages
If you don't need custom functionality, consider using the official React SDK or Next.js SDK instead. They provide a complete, tested solution out of the box.
Overview
This guide will help you build:
- FeatureFlagsProvider: A React Context Provider that manages the SDK instance and flag state
- useFlag Hook: A custom hook to fetch and use individual flags
- useFlags Hook: A custom hook to fetch and use all flags
- Server Utilities: Helper functions for server-side usage in Next.js
The implementation uses React Context API to share the SDK instance and flags across your component tree, eliminating the need to pass props manually.
Getting Started
Install Dependencies
Install the core JavaScript SDK. You only need @basestack/flags-js - no React-specific packages required.
npm install @basestack/flags-jspnpm install @basestack/flags-jsyarn add @basestack/flags-jsbun add @basestack/flags-jsEnvironment Variables
Configure your environment variables. The prefix depends on your build tool (Vite uses VITE_, Next.js uses NEXT_PUBLIC_, etc.).
When it comes to environment variables, pay attention to the framework you're
using. For example, NEXT_PUBLIC_ is specific to Next.js, while in
Vite.js, it would be VITE_. Example
VITE_FEATURE_FLAGS_BASE_URL= or Node.js FEATURE_FLAGS_BASE_URL=
# BASESTACK FEATURE FLAGS
VITE_FEATURE_FLAGS_BASE_URL="https://flags-api.basestack.co/v1"
VITE_FEATURE_FLAGS_PROJECT_KEY=""
VITE_FEATURE_FLAGS_ENVIRONMENT_KEY=""You can find your project and environment keys in your Basestack Feature Flags Dashboard.
Create Project Structure
Create a new folder structure to organize your custom implementation. This keeps your feature flags code organized and reusable.
File Structure:
index.tsx- The FeatureFlagsProvider componentserver.ts- Server-side SDK utilities for Next.jshooks/useFlag.tsx- Hook for fetching individual flagshooks/useFlags.tsx- Hook for fetching all flagshooks/index.ts- Barrel export for hooks
Implementation Guide
Step 1: Create the Provider
The FeatureFlagsProvider is a React Context Provider that wraps your application and manages the SDK instance. It initializes the SDK, fetches all flags on mount, and provides them to child components via context.
Key Features:
- Creates and manages a single SDK instance
- Fetches all flags on initialization
- Tracks initialization state and errors
- Provides SDK client and flags to all child components
"use client";
import React, { createContext, useEffect, useMemo, useState } from "react";
import { FlagsSDK, Flag, SDKConfig } from "@basestack/flags-js";
interface ContextState {
client: FlagsSDK;
isInitialized: boolean;
flags: Flag[];
error?: Error;
}
interface ProviderProps {
children: React.ReactNode;
config: SDKConfig;
onSuccessfulInit?: (success: boolean) => void;
}
// Helper function to create SDK instance
export const createFlagsClient = (config: SDKConfig) => {
return new FlagsSDK({
baseURL: config.baseURL,
projectKey: config.projectKey,
environmentKey: config.environmentKey,
});
};
// Create the context
export const FeatureFlagsContext = createContext<ContextState | undefined>(
undefined,
);
// Provider component
const FeatureFlagsProvider: React.FC<ProviderProps> = ({
children,
config,
onSuccessfulInit,
}) => {
const [state, setState] = useState<Omit<ContextState, "client">>({
isInitialized: false,
flags: [],
});
// Create SDK instance (memoized to prevent recreation)
const client = useMemo(() => createFlagsClient(config), [config]);
// Combine state with client for context value
const value = useMemo(() => ({ ...state, client }), [state, client]);
useEffect(() => {
let isMounted = true;
const initializeFlags = async () => {
try {
// Fetch all flags on initialization
const response = await client.getAllFlags();
if (isMounted) {
setState((prev) => ({
...prev,
flags: response.flags ?? [],
isInitialized: true,
}));
onSuccessfulInit?.(true);
}
} catch (error) {
if (isMounted) {
setState((prev) => ({
...prev,
error: error as Error,
isInitialized: true,
}));
onSuccessfulInit?.(false);
}
}
};
initializeFlags();
// Cleanup function
return () => {
isMounted = false;
};
}, [client, onSuccessfulInit]);
return (
<FeatureFlagsContext.Provider value={value}>
{children}
</FeatureFlagsContext.Provider>
);
};
export default FeatureFlagsProvider;The "use client" directive is required for Next.js App Router. If you're using React only or Next.js Pages Router, you can remove it.
Step 2: Create Server Utilities (Next.js)
For Next.js server-side usage (API routes, Server Components, Server Actions), create a singleton SDK instance. This ensures you reuse the same instance across server requests, improving performance.
import { FlagsSDK, SDKConfig } from "@basestack/flags-js";
export class ServerFlagsSDK {
private static instance: FlagsSDK;
private static config: SDKConfig = {
baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY!,
environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY!,
// For Vite.js, use: import.meta.env.VITE_FEATURE_FLAGS_ENVIRONMENT_KEY!
};
public static getInstance(): FlagsSDK {
if (!ServerFlagsSDK.instance) {
ServerFlagsSDK.instance = new FlagsSDK(ServerFlagsSDK.config);
}
return ServerFlagsSDK.instance;
}
}Why a Singleton?
- Reuses the same SDK instance across server requests
- Maintains cache between requests (if configured)
- Reduces memory usage and initialization overhead
Step 3: Create the useFlag Hook
The useFlag hook fetches a single flag by its slug. It first checks the preloaded flags from context, and if not found, fetches it directly from the SDK.
Features:
- Type-safe payload support via TypeScript generics
- Automatic flag lookup in preloaded flags
- Fallback to direct SDK fetch if flag not preloaded
- Error handling and loading states
"use client";
import { useContext, useEffect, useState, useMemo } from "react";
import { FeatureFlagsContext } from "@/libs/feature-flags";
import { Flag } from "@basestack/flags-js";
export const useFlag = <P = Record<string, unknown>,>(slug: string) => {
const context = useContext(FeatureFlagsContext);
const [flag, setFlag] = useState<Flag | null>(null);
const [error, setError] = useState<Error | null>(null);
if (!context) {
throw new Error("useFlag must be used within a FeatureFlagsContext");
}
useEffect(() => {
const fetchFlag = async () => {
if (!context.isInitialized || !context.client) return;
try {
// First, check if flag exists in preloaded flags
if (context.flags.length > 0) {
const foundFlag = context.flags.find((f) => f.slug === slug);
if (foundFlag) {
setFlag(foundFlag);
return;
}
// Flag not found in preloaded flags
setError(new Error(`Flag "${slug}" not found`));
} else {
// No preloaded flags, fetch directly from SDK
const fetchedFlag = await context.client.getFlag(slug);
setFlag(fetchedFlag);
}
} catch (err) {
setError(err instanceof Error ? err : new Error(String(err)));
}
};
void fetchFlag();
}, [context.isInitialized, context.client, context.flags, slug]);
return useMemo(
() => ({
...flag,
error,
isInitialized: context.isInitialized,
payload: (flag?.payload as P) ?? null,
}),
[error, context.isInitialized, flag],
);
};Usage:
// Basic usage
const { enabled, payload } = useFlag("header");
// With TypeScript payload typing
const { enabled, payload } = useFlag<{ variant: string }>("header");Step 4: Create the useFlags Hook
The useFlags hook retrieves all flags for the current environment. It uses preloaded flags from context when available, otherwise fetches them directly.
Features:
- Returns all flags in an array
- Uses preloaded flags when available
- Provides loading and error states
"use client";
import { useContext, useEffect, useState, useMemo } from "react";
import { FeatureFlagsContext } from "@/libs/feature-flags";
import { Flag } from "@basestack/flags-js";
interface FlagsState {
flags: Flag[];
}
interface UseFlagsResult extends FlagsState {
error: Error | null;
isInitialized: boolean;
}
export const useFlags = (): UseFlagsResult => {
const context = useContext(FeatureFlagsContext);
const [{ flags }, setFlags] = useState<FlagsState>({ flags: [] });
const [error, setError] = useState<Error | null>(null);
if (!context) {
throw new Error("useFlags must be used within a FeatureFlagsContext");
}
useEffect(() => {
if (!context.isInitialized || !context.client) return;
// Use preloaded flags if available
if (context.flags.length > 0) {
setFlags({ flags: context.flags });
return;
}
// Otherwise, fetch all flags
context.client
.getAllFlags()
.then((result) => setFlags(result))
.catch((err) =>
setError(err instanceof Error ? err : new Error(String(err))),
);
}, [
context.isInitialized,
context.client,
context.flags.length,
context.flags,
]);
return useMemo(
() => ({
error,
isInitialized: context.isInitialized,
flags,
}),
[flags, context.isInitialized, error],
);
};Step 5: Export Hooks
Create a barrel export file for easy imports:
export * from "./useFlag";
export * from "./useFlags";Now you can import hooks like this:
import { useFlag, useFlags } from "@/libs/feature-flags/hooks";Setup Your Application
Next.js App Router
Wrap your application with the FeatureFlagsProvider in your root layout:
import React from "react";
import "./globals.css";
import FeatureFlagsProvider from "@/libs/feature-flags";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<FeatureFlagsProvider
config={{
baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY!,
environmentKey:
process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY!,
}}
>
{children}
</FeatureFlagsProvider>
</body>
</html>
);
}Next.js Pages Router
Add the provider in your _app.tsx:
import type { AppProps } from "next/app";
import FeatureFlagsProvider from "@/libs/feature-flags";
export default function App({ Component, pageProps }: AppProps) {
return (
<FeatureFlagsProvider
config={{
baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY!,
environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY!,
}}
>
<Component {...pageProps} />
</FeatureFlagsProvider>
);
}React SPA (Vite, Create React App, etc.)
Add the provider in your root component:
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import FeatureFlagsProvider from "@/libs/feature-flags";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<FeatureFlagsProvider
config={{
baseURL: import.meta.env.VITE_FEATURE_FLAGS_BASE_URL,
projectKey: import.meta.env.VITE_FEATURE_FLAGS_PROJECT_KEY!,
environmentKey: import.meta.env.VITE_FEATURE_FLAGS_ENVIRONMENT_KEY!,
}}
>
<App />
</FeatureFlagsProvider>
</React.StrictMode>,
);Usage Examples
Using Hooks in Components
Use the hooks in any client component within your provider tree:
"use client";
import { useFlag, useFlags } from "@/libs/feature-flags/hooks";
export default function Home() {
// Fetch a single flag with TypeScript payload typing
const headerFlag = useFlag<{ variant: string; showLogo: boolean }>("header");
// Fetch all flags
const { flags, isLoading, error } = useFlags();
if (isLoading) {
return <div>Loading feature flags...</div>;
}
if (error) {
return <div>Error loading flags: {error.message}</div>;
}
return (
<div>
<main>
{/* Use individual flag */}
{headerFlag.enabled && (
<header>
<h1>Header Feature Enabled</h1>
{headerFlag.payload?.variant && (
<p>Variant: {headerFlag.payload.variant}</p>
)}
</header>
)}
{/* Display all flags */}
<h3>All Available Flags</h3>
<ul>
{flags.map((flag) => (
<li key={flag.slug}>
{flag.slug}: {flag.enabled ? "enabled" : "disabled"}
</li>
))}
</ul>
</main>
</div>
);
}Key Points:
- Use
useFlag<PayloadType>for type-safe payloads - Check
isLoadinganderrorstates for better UX - Flags are automatically available from the provider context
Server-Side Usage (Next.js)
API Route Handler (App Router)
Use the server SDK instance in API routes:
import { ServerFlagsSDK } from "@/libs/feature-flags/server";
export async function GET() {
const flagsClient = ServerFlagsSDK.getInstance();
try {
const flag = await flagsClient.getFlag("header");
const { flags } = await flagsClient.getAllFlags();
return Response.json({
flag,
totalFlags: flags.length
});
} catch (error) {
return Response.json(
{ error: "Failed to fetch flags" },
{ status: 500 }
);
}
}API Endpoint (Pages Router)
Use in Pages Router API routes:
import type { NextApiRequest, NextApiResponse } from "next";
import { ServerFlagsSDK } from "@/libs/feature-flags/server";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const flagsClient = ServerFlagsSDK.getInstance();
try {
const flag = await flagsClient.getFlag("header");
const { flags } = await flagsClient.getAllFlags();
res.status(200).json({ flag, totalFlags: flags.length });
} catch (error) {
res.status(500).json({ error: "Failed to fetch flags" });
}
}Server Components
Use in Next.js Server Components:
import { ServerFlagsSDK } from "@/libs/feature-flags/server";
export default async function FlagsPage() {
const flagsClient = ServerFlagsSDK.getInstance();
const { flags } = await flagsClient.getAllFlags();
return (
<div>
<h1>Feature Flags</h1>
<ul>
{flags.map((flag) => (
<li key={flag.slug}>
<strong>{flag.slug}</strong>: {flag.enabled ? "enabled" : "disabled"}
</li>
))}
</ul>
</div>
);
}Advanced: Direct Context Access
For advanced use cases, access the SDK client directly from context:
"use client";
import { useState, useEffect, useContext } from "react";
import { FeatureFlagsContext } from "@/libs/feature-flags";
import type { Flag } from "@basestack/flags-js";
export default function Posts() {
const [flags, setFlags] = useState<Flag[]>([]);
const context = useContext(FeatureFlagsContext);
useEffect(() => {
async function fetchAllFlags() {
if (!context?.client) return;
try {
const { flags } = await context.client.getAllFlags();
setFlags(flags);
} catch (error) {
console.error("Failed to fetch flags:", error);
}
}
fetchAllFlags();
}, [context]);
if (flags.length === 0) return <div>Loading...</div>;
return (
<ul>
{flags.map((flag) => (
<li key={flag.slug}>
{flag.slug}: {flag.enabled ? "enabled" : "disabled"}
</li>
))}
</ul>
);
}Customization Ideas
Since you have full control over the implementation, you can easily customize it:
Add Analytics Tracking
// In useFlag hook
useEffect(() => {
if (flag?.enabled) {
analytics.track("flag_checked", { slug, enabled: flag.enabled });
}
}, [flag, slug]);Add Custom Caching Logic
// In Provider
const [cache, setCache] = useState<Map<string, Flag>>(new Map());
// Custom cache management
const getCachedFlag = (slug: string) => {
return cache.get(slug);
};Add Refresh Functionality
// In useFlag hook
const refresh = async () => {
const freshFlag = await context.client.getFlag(slug);
setFlag(freshFlag);
};
return { ...flag, refresh };Best Practices
- Initialize Early: Wrap your app with the provider as high as possible in the component tree
- Handle Loading States: Always check
isInitializedandisLoadingbefore using flags - Error Handling: Provide fallback behavior when flags fail to load
- Type Safety: Use TypeScript generics for payload types
- Server Singleton: Use the singleton pattern for server-side SDK instances
Initialisation Options
For details regarding the initialization options of @basestack/flags-js, please refer to the JavaScript SDK documentation.
Troubleshooting
Components Rendering Twice
If your components are rendering twice, this is likely due to React.StrictMode in development. This is expected behavior and helps identify potential issues.
Solution: This is normal in development. In production, components will only render once. If you want to disable it:
const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(
// Remove StrictMode wrapper if needed
<FeatureFlagsProvider config={config}>
<App />
</FeatureFlagsProvider>
);Hook Used Outside Provider
If you see the error "useFlag must be used within a FeatureFlagsContext":
Solution: Ensure your component is wrapped with FeatureFlagsProvider:
// ✅ Correct
<FeatureFlagsProvider config={config}>
<YourComponent />
</FeatureFlagsProvider>
// ❌ Wrong - component outside provider
<YourComponent />Flags Not Loading
If flags aren't loading:
- Check Environment Variables: Ensure they're correctly prefixed (
NEXT_PUBLIC_for Next.js,VITE_for Vite) - Verify Provider Setup: Make sure
FeatureFlagsProviderwraps your entire app - Check Network: Verify your app can reach the Basestack API
- Check Console: Look for error messages in the browser console
TypeScript Errors
If you're getting TypeScript errors:
- Check Imports: Ensure you're importing from the correct paths
- Type Payloads: Use generics:
useFlag<YourPayloadType>("slug") - Install Types: Types are included with
@basestack/flags-js
Next Steps
- Explore the JavaScript SDK documentation for more SDK features
- Check out the official React SDK if you prefer a pre-built solution