Basestack Docs

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:

  1. FeatureFlagsProvider: A React Context Provider that manages the SDK instance and flag state
  2. useFlag Hook: A custom hook to fetch and use individual flags
  3. useFlags Hook: A custom hook to fetch and use all flags
  4. 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.

Install the SDK
npm install @basestack/flags-js
pnpm install @basestack/flags-js
yarn add @basestack/flags-js
bun add @basestack/flags-js

Environment 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=

.env
# 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.

useFlag.tsx
useFlags.tsx
index.ts
index.tsx
server.ts
.env

File Structure:

  • index.tsx - The FeatureFlagsProvider component
  • server.ts - Server-side SDK utilities for Next.js
  • hooks/useFlag.tsx - Hook for fetching individual flags
  • hooks/useFlags.tsx - Hook for fetching all flags
  • hooks/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
src/libs/feature-flags/index.tsx
"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.

src/libs/feature-flags/server.ts
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
src/libs/feature-flags/hooks/useFlag.tsx
"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
src/libs/feature-flags/hooks/useFlags.tsx
"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:

src/libs/feature-flags/hooks/index.ts
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:

src/app/layout.tsx
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:

pages/_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:

src/main.tsx
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:

src/app/page.tsx
"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 isLoading and error states 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:

src/app/api/flags/route.ts
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:

src/pages/api/flags.ts
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:

src/app/flags/page.tsx
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:

src/app/posts/page.tsx
"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

  1. Initialize Early: Wrap your app with the provider as high as possible in the component tree
  2. Handle Loading States: Always check isInitialized and isLoading before using flags
  3. Error Handling: Provide fallback behavior when flags fail to load
  4. Type Safety: Use TypeScript generics for payload types
  5. 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:

src/main.tsx
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:

  1. Check Environment Variables: Ensure they're correctly prefixed (NEXT_PUBLIC_ for Next.js, VITE_ for Vite)
  2. Verify Provider Setup: Make sure FeatureFlagsProvider wraps your entire app
  3. Check Network: Verify your app can reach the Basestack API
  4. Check Console: Look for error messages in the browser console

TypeScript Errors

If you're getting TypeScript errors:

  1. Check Imports: Ensure you're importing from the correct paths
  2. Type Payloads: Use generics: useFlag<YourPayloadType>("slug")
  3. Install Types: Types are included with @basestack/flags-js

Next Steps