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 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
tsdownand linted/formatted via Biome.
bun install @basestack/flags-react @basestack/flags-jsnpm install @basestack/flags-react @basestack/flags-jsyarn add @basestack/flags-react @basestack/flags-jsReact 18+ is required and should already exist in your project. The package ships as pure ESM and targets modern browsers/runtime APIs.
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 { 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
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,readHydratedFlags, 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-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.
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 } 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>
);
},
});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 }. - Automatically fetches the flag once per mount (unless
options.fetch === false). options.defaultEnabledandoptions.defaultPayloadlet you provide fallbacks while loading.
- 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
All server helpers live in the /server subpath:
import {
fetchFlags,
fetchFlag,
createServerFlagsClient,
} from "@basestack/flags-react/server";fetchFlags(config, slugs?): 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.
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.
| 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/Node 20+ 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.