Skip to content

basestack-co/basestack-flags-react

Repository files navigation

Basestack Feature Flags React Integration

React bindings for the Basestack Flags JS SDK. This package exposes a provider, hooks, hydration helpers, and SSR utilities that work across Vite, Next.js (App or Pages Router), and TanStack Start.

Features

  • Zero-config provider powered by the official @basestack/flags-js client.
  • Hooks and components for component-level reads (useFlag, useFlags, useFlagsClient, Feature).
  • Server utilities to preload flags in frameworks with data loaders or RSC.
  • Hydration helpers for streaming initial flag snapshots safely to the client.
  • Tree-shakeable ESM output built with tsdown and linted/formatted via Biome.

Installation

bun install @basestack/flags-react
npm install @basestack/flags-react
yarn add @basestack/flags-react

React 19.2.4+ is required. react-dom 18.2.0+ is recommended for browser apps and remains optional for server-only consumers. The package ships as pure ESM and targets modern browsers/runtime APIs.

Runtime requirements:

  • Node.js 18.17+ for local tooling and server runtimes.
  • An ESM-compatible framework or bundler.

You can import SDKConfig, Flag, CacheConfig, and FlagsSDK directly from this package (@basestack/flags-react, @basestack/flags-react/client, or @basestack/flags-react/server) without adding a direct dependency on @basestack/flags-js.

Local development

The repository uses Bun as the package manager and script runner:

bun install           # install dependencies
bun run lint          # biome lint (restricted to src + config files)
bun run test          # vitest suite
bun run build         # compile to dist/ via tsdown

All examples rely on the compiled dist/ output, so run bun run build before opening any of them.

Quick start (React + Vite)

import { Feature, FlagsProvider, useFlag } from "@basestack/flags-react/client";

const config = {
  projectKey: process.env.VITE_BASESTACK_PROJECT_KEY!,
  environmentKey: process.env.VITE_BASESTACK_ENVIRONMENT_KEY!,
};

function App() {
  return (
    <FlagsProvider config={config}>
      <HomePage />
    </FlagsProvider>
  );
}

function HomePage() {
  const { enabled, payload, isLoading } = useFlag<{ variant: string }>(
    "header"
  );

  if (isLoading) return <p>Loading…</p>;
  return enabled ? (
    <NewHomepage variant={payload?.variant} />
  ) : (
    <LegacyHomepage />
  );
}

For simple render gating, the component API can be even smaller:

import { Feature } from "@basestack/flags-react/client";

function MarketingPage() {
  return (
    <Feature slug="marketing-callout">
      <article className="card accent">
        <h2>Component reference</h2>
        <p>marketing-callout is wrapped in a Feature component.</p>
      </article>
    </Feature>
  );
}
  • The provider accepts the exact SDKConfig used by @basestack/flags-js plus optional props:
    • initialFlags: preload data, usually from SSR.
    • preload (default true): automatically fetch missing flags when initialFlags is empty.
    • onError: observe network/caching errors.
  • Hooks keep a shared cache, so subsequent components reuse already fetched flags.
  • Call refresh() from either useFlag or useFlags to re-query the API.

Import paths

Use the subpath that matches your runtime to avoid loading client-only hooks on the server:

  • @basestack/flags-react/clientFlagsProvider, hooks, Feature, readHydratedFlags, preview helpers, modal helpers, and SDK types. The file itself includes the "use client" directive.
  • @basestack/flags-react/serverfetchFlag, fetchFlags, createServerFlagsClient, FlagsHydrationScript, and shared constants.
  • @basestack/flags-react — server-friendly exports (no hooks or provider). Prefer the explicit /client and /server paths for new integrations.

Next.js (App Router)

// app/flags-config.ts
export const flagsConfig = {
  baseURL: process.env.BASESTACK_BASE_URL!,
  projectKey: process.env.BASESTACK_PROJECT_KEY!,
  environmentKey: process.env.BASESTACK_ENVIRONMENT_KEY!,
};
// app/layout.tsx
import {
  FlagsHydrationScript,
  fetchFlags,
} from "@basestack/flags-react/server";
import { Providers } from "./providers";
import { flagsConfig } from "./flags-config";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const flags = await fetchFlags(flagsConfig);

  return (
    <html lang="en">
      <body>
        <Providers initialFlags={flags}>{children}</Providers>
        <FlagsHydrationScript flags={flags} />
      </body>
    </html>
  );
}
// app/providers.tsx
"use client";

import { FlagsProvider } from "@basestack/flags-react/client";
import type { Flag } from "@basestack/flags-react";
import type { ReactNode } from "react";
import { flagsConfig } from "./flags-config";

export function Providers({
  children,
  initialFlags,
}: {
  children: ReactNode;
  initialFlags?: Flag[];
}) {
  return (
    <FlagsProvider
      config={flagsConfig}
      initialFlags={initialFlags}
      preload={!initialFlags?.length}
    >
      {children}
    </FlagsProvider>
  );
}

Use fetchFlag() inside Server Components or Route Handlers if you only need a single slug.

Route Handler + Server Functions demo

The App Router example also includes:

  • GET /api/flags (app/api/flags/route.ts) to prove the SDK works inside a Route Handler / API route.
  • A /server-functions page that lists current flag states on the server and ships a ServerActionDemo client component which invokes a server action powered by fetchFlag.

Next.js (Pages Router)

// pages/_app.tsx
import type { AppProps } from "next/app";
import { FlagsProvider, type Flag } from "@basestack/flags-react/client";
import type { Flag } from "@basestack/flags-react";

const config = {
  projectKey: process.env.NEXT_PUBLIC_BASESTACK_PROJECT_KEY!,
  environmentKey: process.env.NEXT_PUBLIC_BASESTACK_ENVIRONMENT_KEY!,
};

export default function MyApp({
  Component,
  pageProps,
}: AppProps<{ flags?: Flag[] }>) {
  const initialFlags = pageProps.flags ?? [];

  return (
    <FlagsProvider
      config={config}
      initialFlags={initialFlags}
      preload={!initialFlags.length}
    >
      <Component {...pageProps} />
    </FlagsProvider>
  );
}
// pages/index.tsx
import { fetchFlags } from "@basestack/flags-react/server";
import { useFlag } from "@basestack/flags-react/client";
import type { GetServerSideProps } from "next";
import type { Flag } from "@basestack/flags-react";

export const getServerSideProps: GetServerSideProps<{ flags: Flag[] }> = async () => {
  const flags = await fetchFlags({
    baseURL: process.env.BASESTACK_BASE_URL!,
    projectKey: process.env.BASESTACK_PROJECT_KEY!,
    environmentKey: process.env.BASESTACK_ENVIRONMENT_KEY!,
  });

  return {
    props: { flags },
  };
};

export default function HomePage() {
  const { enabled, isLoading } = useFlag("header");

  if (isLoading) return <p>Checking...</p>;
  return enabled ? <NewHomepage /> : <LegacyHomepage />;
}

API Route

Add a legacy API route that relies on the same server helper:

// pages/api/flags.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { fetchFlags } from "@basestack/flags-react/server";
import { flagsConfig } from "../../flags-config";

export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
  try {
    const flags = await fetchFlags(flagsConfig);
    res.status(200).json({ flags });
  } catch (error) {
    res.status(500).json({ message: "Unable to load flags" });
  }
}

TanStack Start

// app/config/flags.ts
export const flagsConfig = {
  projectKey: process.env.BASESTACK_PROJECT_KEY!,
  environmentKey: process.env.BASESTACK_ENVIRONMENT_KEY!,
};
// routes/_app.tsx
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { FlagsProvider } from "@basestack/flags-react/client";
import { fetchFlags } from "@basestack/flags-react/server";
import { flagsConfig } from "../config/flags";

export const Route = createFileRoute("/_app")({
  loader: async () => ({ flags: await fetchFlags(flagsConfig) }),
  component: () => {
    const { flags } = Route.useLoaderData();
    return (
      <FlagsProvider config={flagsConfig} initialFlags={flags} preload={false}>
        <Outlet />
      </FlagsProvider>
    );
  },
});

React + Vite (with server prefetch)

When running a Vite app locally you can hydrate the provider with data fetched from your backend (or from the included Node dev server):

// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { FlagsProvider } from "@basestack/flags-react/client";
import { fetchFlags } from "@basestack/flags-react/server";
import { App } from "./App";
import { flagsConfig } from "./flagsConfig";

async function bootstrap() {
  const container = document.getElementById("root");
  if (!container) throw new Error("Missing #root");

  let initialFlags = [];
  try {
    initialFlags = await fetchFlags(flagsConfig);
  } catch (error) {
    console.warn("Failed to preload flags", error);
  }

  createRoot(container).render(
    <StrictMode>
      <FlagsProvider
        config={flagsConfig}
        initialFlags={initialFlags}
        preload={initialFlags.length === 0}
      >
        <App />
      </FlagsProvider>
    </StrictMode>
  );
}

bootstrap();
// src/App.tsx
import { useFlag } from "@basestack/flags-react/client";

export function App() {
  const { enabled, payload, isLoading } = useFlag<{ variant?: string }>(
    "header"
  );

  if (isLoading) return <p>Checking...</p>;
  return enabled ? (
    <NewHomepage variant={payload?.variant} />
  ) : (
    <LegacyHomepage />
  );
}

Hooks reference

Import these from @basestack/flags-react/client.

  • useFlag(slug, options)
    • Returns { flag, enabled, payload, isLoading, error, refresh, openFeedbackModal }.
    • Automatically fetches the flag once per mount (unless options.fetch === false).
    • options.defaultEnabled and options.defaultPayload let you provide fallbacks while loading.
    • Preview state stored in localStorage can force enabled === true for a slug during client-side previews.
    • openFeedbackModal() is a no-op unless FeatureFlagModalsProvider is mounted.
  • useFlags()
    • Returns { flags, flagsBySlug, isLoading, error, refresh }.
    • Ideal for Admin/Settings UIs or debugging views.
  • useFlagsClient()
    • Provides direct access to the underlying FlagsSDK instance for advanced operations.

Component reference

  • Feature
    • Default usage is <Feature slug="flag-slug">{children}</Feature>.
    • Plain JSX children render only when the flag is enabled.
    • fallback renders when the flag is disabled.
    • loading renders while the flag is being resolved.
    • defaultEnabled, defaultPayload, and fetch behave the same as useFlag.
    • When children, fallback, or loading are functions, they receive the same object returned by useFlag.
import { Feature } from "@basestack/flags-react/client";

<Feature
  slug="marketing-callout"
  fallback={<LegacyCallout />}
  loading={<p>Checking feature flag…</p>}
>
  <MarketingCallout />
</Feature>;

Use the render-prop form when you need access to payload, errors, or actions such as refresh() and openFeedbackModal():

import { Feature } from "@basestack/flags-react/client";

<Feature<{ variant?: string }>
  slug="marketing-callout"
  defaultPayload={{ variant: "control" }}
>
  {({ enabled, payload, isLoading, error, refresh, openFeedbackModal }) => {
    if (isLoading) return <p>Checking feature flag…</p>;
    if (error) return <button onClick={() => refresh()}>Retry</button>;
    if (!enabled) return <LegacyCallout />;

    return (
      <section>
        <MarketingCallout variant={payload?.variant} />
        <button
          type="button"
          onClick={() => openFeedbackModal({ featureName: "Marketing callout" })}
        >
          Leave feedback
        </button>
      </section>
    );
  }}
</Feature>;

Server utilities

All server helpers live in the /server subpath:

import {
  fetchFlags,
  fetchFlag,
  createServerFlagsClient,
} from "@basestack/flags-react/server";
  • fetchFlags(config, slugs?, { fallback, onError }): returns a Flag[]. When slugs is omitted, it loads the full project.
  • fetchFlag(slug, config): fetch exactly one flag.
  • createServerFlagsClient(config): returns a configured FlagsSDK so you can call low-level methods inside loaders.

When you pass slugs, fetchFlags() requires every slug fetch to succeed. If any request fails, it calls onError and returns fallback (or [] when no fallback is provided).

Hydration helpers

import { FlagsHydrationScript } from "@basestack/flags-react/server";
import { readHydratedFlags } from "@basestack/flags-react/client";

// Server: embed the payload after the provider so client components can read it
<FlagsHydrationScript flags={flags} globalKey="__BASESTACK_FLAGS__" />;

// Client: read during bootstrapping (before rendering) if you need to avoid prop-drilling
const hydrated = readHydratedFlags();

FlagsHydrationScript encodes the snapshot using globalThis["__BASESTACK_FLAGS__"]. Pass globalKey to customize the name or set a CSP nonce when needed. readHydratedFlags only works in the browser, so import it from /client.

Preview state

The client entry point also exports:

  • BS_FLAGS_PREVIEW_STATE_KEY
  • getPreviewState()

These are used by the web-component preview flow. If localStorage contains bs-flags-preview-state, useFlag() treats any slug marked true as enabled on the client, even if the fetched flag is disabled. This is useful for preview/testing flows and worth knowing when debugging unexpected enabled states.

Modal integration

The client package includes optional helpers built on top of @basestack/flags-wc:

  • FeatureFlagModalsProvider
  • useFeatureFlagModals()
  • useFeatureFlagModalsOptional()
  • OpenFeedbackModalOptions
  • FeatureFlagModalsConfig

Wrap your app with FeatureFlagModalsProvider inside FlagsProvider when you want preview and feedback modals:

import {
  FeatureFlagModalsProvider,
  FlagsProvider,
  useFeatureFlagModals,
} from "@basestack/flags-react/client";

const flagsConfig = {
  projectKey: process.env.VITE_BASESTACK_PROJECT_KEY!,
  environmentKey: process.env.VITE_BASESTACK_ENVIRONMENT_KEY!,
};

const modalConfig = {
  preview: {},
  feedback: {},
};

function AppShell() {
  return (
    <FlagsProvider config={flagsConfig}>
      <FeatureFlagModalsProvider config={modalConfig}>
        <App />
      </FeatureFlagModalsProvider>
    </FlagsProvider>
  );
}

function PreviewButton() {
  const { ready, error, openPreviewModal } = useFeatureFlagModals();

  if (error) return <p>Preview UI unavailable.</p>;

  return (
    <button type="button" disabled={!ready} onClick={() => openPreviewModal()}>
      Open flag preview
    </button>
  );
}

Notes:

  • FeatureFlagModalsProvider must be rendered under FlagsProvider.
  • ready becomes true after the custom elements from @basestack/flags-wc are registered.
  • onError receives registration failures.
  • By default, the provider derives projectKey, environmentKey, and baseURL from FlagsProvider.
  • Default endpoints are ${baseURL}/flags/preview for preview and ${baseURL}/flags/preview/feedback for feedback.
  • useFlag(...).openFeedbackModal(options) forwards the current flag slug plus optional featureName and metadata.

Scripts

Command Description
bun run build Bundle ESM + type declarations with tsdown
bun run dev Watch-mode build for local development
bun run lint Run Biome lint rules
bun run format Format the entire repo with Biome
bun run test Execute the Vitest suite in JSDOM

Use bun run prepublishOnly locally before releasing to ensure lint + tests stay green.

Development notes

  • Source lives in src/ and is compiled to dist/ via tsdown (ESM only).
  • The package exposes only modern ESM syntax; no CommonJS output is produced.
  • Biome powers linting/formatting, so please keep editor integrations enabled.

Examples

Minimal framework demos live in examples/. Each project links @basestack/flags-react and the /client + /server subpaths to dist/, so you can test the SDK locally without publishing.

Example Highlights Path Dev command
Next.js 16 App Router Provider wrapper, Route Handler (GET /api/flags), /server-functions page with Server Action demo examples/next-app-router bun run dev
Next.js 16 Pages Router _app wiring, getServerSideProps, pages/api/flags.ts API route examples/next-pages-router bun run dev
React + Vite Client-only bootstrap that preloads flags before render examples/react-vite bun run dev

To run an example:

  1. bun run build at the repo root (ensures dist/ exists).
  2. cd examples/<example> and bun install.
  3. Provide BASESTACK_* environment variables (or use the demo IDs committed in each config).
  4. bun run dev to start the framework’s dev server.

See examples/README.md for more context.