@basestack/flags-react
v1.2.3
Published
React bindings for the Basestack Feature Flags SDK
Downloads
281
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 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.
Installation
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.
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 { 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.
Import paths
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.
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-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-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, 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 />;
}API Route
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" });
}
}TanStack Start
// 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, 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
Component reference
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>;Server utilities
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).
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.
Preview state
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.
Modal integration
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.
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 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.
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 [email protected].
