@basestack/flags-react
v1.0.5
Published
React bindings for the Basestack Feature Flags SDK
Downloads
23
Maintainers
Readme
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-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.
Installation
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.
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 tsdownAll 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
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.
Import paths
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.
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-functionspage that lists current flag states on the server and ships aServerActionDemoclient component which invokes a server action powered byfetchFlag.
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.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
Server utilities
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.
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 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.
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:
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.
