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 for component-level reads (useFlag, useFlags, useFlagsClient).
  • 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 @basestack/flags-js
npm install @basestack/flags-react @basestack/flags-js
yarn add @basestack/flags-react @basestack/flags-js

React 18+ is required and should already exist in your project. The package ships as pure ESM and targets modern browsers/runtime APIs.

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 { 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 />
  );
}
  • 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, readHydratedFlags, 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-js";
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 } from "@basestack/flags-react/client";

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-js";

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 },
  };
};

### API Route

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

```ts
// 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

```tsx
// 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 }.
    • Automatically fetches the flag once per mount (unless options.fetch === false).
    • options.defaultEnabled and options.defaultPayload let you provide fallbacks while loading.
  • 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.

Server utilities

All server helpers live in the /server subpath:

import {
  fetchFlags,
  fetchFlag,
  createServerFlagsClient,
} from "@basestack/flags-react/server";
  • fetchFlags(config, slugs?): 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.

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.

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/Node 20+ 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.