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.
- Zero-config provider powered by the official
@basestack/flags-jsclient. - 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
tsdownand linted/formatted via Biome.
bun install @basestack/flags-reactnpm install @basestack/flags-reactyarn add @basestack/flags-reactReact 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.
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 tsdownAll examples rely on the compiled dist/ output, so run bun run build before opening any of them.
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
SDKConfigused by@basestack/flags-jsplus optional props:initialFlags: preload data, usually from SSR.preload(defaulttrue): automatically fetch missing flags wheninitialFlagsis empty.onError: observe network/caching errors.
- Hooks keep a shared cache, so subsequent components reuse already fetched flags.
- Call
refresh()from eitheruseFlagoruseFlagsto re-query the API.
Use the subpath that matches your runtime to avoid loading client-only hooks on the server:
@basestack/flags-react/client—FlagsProvider, hooks,Feature,readHydratedFlags, preview helpers, modal helpers, and SDK types. The file itself includes the"use client"directive.@basestack/flags-react/server—fetchFlag,fetchFlags,createServerFlagsClient,FlagsHydrationScript, and shared constants.@basestack/flags-react— server-friendly exports (no hooks or provider). Prefer the explicit/clientand/serverpaths for new integrations.
// 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.
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-functionspage that lists current flag states on the server and ships aServerActionDemoclient component which invokes a server action powered byfetchFlag.
// 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 />;
}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" });
}
}// 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>
);
},
});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 />
);
}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.defaultEnabledandoptions.defaultPayloadlet you provide fallbacks while loading.- Preview state stored in localStorage can force
enabled === truefor a slug during client-side previews. openFeedbackModal()is a no-op unlessFeatureFlagModalsProvideris mounted.
- Returns
useFlags()- Returns
{ flags, flagsBySlug, isLoading, error, refresh }. - Ideal for Admin/Settings UIs or debugging views.
- Returns
useFlagsClient()- Provides direct access to the underlying
FlagsSDKinstance for advanced operations.
- Provides direct access to the underlying
Feature- Default usage is
<Feature slug="flag-slug">{children}</Feature>. - Plain JSX children render only when the flag is enabled.
fallbackrenders when the flag is disabled.loadingrenders while the flag is being resolved.defaultEnabled,defaultPayload, andfetchbehave the same asuseFlag.- When
children,fallback, orloadingare functions, they receive the same object returned byuseFlag.
- Default usage is
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>;All server helpers live in the /server subpath:
import {
fetchFlags,
fetchFlag,
createServerFlagsClient,
} from "@basestack/flags-react/server";fetchFlags(config, slugs?, { fallback, onError }): returns aFlag[]. Whenslugsis omitted, it loads the full project.fetchFlag(slug, config): fetch exactly one flag.createServerFlagsClient(config): returns a configuredFlagsSDKso 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).
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.
The client entry point also exports:
BS_FLAGS_PREVIEW_STATE_KEYgetPreviewState()
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.
The client package includes optional helpers built on top of @basestack/flags-wc:
FeatureFlagModalsProvideruseFeatureFlagModals()useFeatureFlagModalsOptional()OpenFeedbackModalOptionsFeatureFlagModalsConfig
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:
FeatureFlagModalsProvidermust be rendered underFlagsProvider.readybecomestrueafter the custom elements from@basestack/flags-wcare registered.onErrorreceives registration failures.- By default, the provider derives
projectKey,environmentKey, andbaseURLfromFlagsProvider. - Default endpoints are
${baseURL}/flags/previewfor preview and${baseURL}/flags/preview/feedbackfor feedback. useFlag(...).openFeedbackModal(options)forwards the current flag slug plus optionalfeatureNameandmetadata.
| 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.
- Source lives in
src/and is compiled todist/viatsdown(ESM only). - The package exposes only modern ESM syntax; no CommonJS output is produced.
- Biome powers linting/formatting, so please keep editor integrations enabled.
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:
bun run buildat the repo root (ensuresdist/exists).cd examples/<example>andbun install.- Provide
BASESTACK_*environment variables (or use the demo IDs committed in each config). bun run devto start the framework’s dev server.
See examples/README.md for more context.