# Basestack Feature Flags React Integration

React bindings for the [Basestack Flags JS SDK](https://github.com/basestack-co/basestack-flags-js). 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`](https://github.com/egoist/tsdown) and linted/formatted via [Biome](https://biomejs.dev/).

## Installation

```bash
bun install @basestack/flags-react
```

```bash
npm install @basestack/flags-react
```

```bash
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:

```bash
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)

```tsx
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:

```tsx
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/client` &mdash; `FlagsProvider`, hooks, `Feature`, `readHydratedFlags`, preview helpers, modal helpers, and SDK types. The file itself includes the `"use client"` directive.
- `@basestack/flags-react/server` &mdash; `fetchFlag`, `fetchFlags`, `createServerFlagsClient`, `FlagsHydrationScript`, and shared constants.
- `@basestack/flags-react` &mdash; server-friendly exports (no hooks or provider). Prefer the explicit `/client` and `/server` paths for new integrations.

## Next.js (App Router)

```tsx
// 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!,
};
```

```tsx
// 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>
  );
}
```

```tsx
// 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)

```tsx
// 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>
  );
}
```

```tsx
// 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:

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

```tsx
// 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):

```tsx
// 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();
```

```tsx
// 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`.

```tsx
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()`:

```tsx
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:

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

```tsx
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:

```tsx
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.
-->


> This SDK is distributed under the MIT License. Development is maintained in a private monorepo, and releases are published to npm as the official distribution.
>
> For bug reports, support requests, or security concerns, please contact support@basestack.co.
