Next.js (Official SDK)
Seamless integration with Next.js streamlines the testing and development of features across production and various environments. This SDK effortlessly integrates into any Next.js-based environment, allowing you to leverage this capability in projects like Next.js App Router or Pages Router.
The Basestack Next.js SDK provides optimized server-side rendering (SSR) support, enabling you to fetch feature flags on the server and hydrate them on the client. This approach eliminates loading states, improves SEO, and ensures consistent flag values between server and client renders.
Getting started
Examples
Install
First, let's install some packages! The Next.js SDK requires both @basestack/flags-react for React-specific functionality and @basestack/flags-js as the core SDK dependency.
npm install @basestack/flags-react @basestack/flags-jspnpm install @basestack/flags-react @basestack/flags-jsyarn add @basestack/flags-react @basestack/flags-jsbun add @basestack/flags-react @basestack/flags-jsEnvironment Variables
Configure your environment variables to connect to your Basestack project. The NEXT_PUBLIC_ prefix makes these variables available in both server and client components.
Make sure to add your .env file to .gitignore to keep your keys secure. Never commit sensitive credentials to version control.
# BASESTACK FEATURE FLAGS
NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL="https://flags.basestack.co/api/v1" # or your own URL
NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY=""
NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY=""You can find your project and environment keys in your Basestack Feature Flags Dashboard.
App Router Implementation
The App Router is Next.js's modern routing system that provides better performance and developer experience. This implementation leverages server components to fetch flags on the server, reducing client-side loading states and improving initial page load performance.
Setup the Provider
First, create a client-side provider component that wraps your application. This provider manages the feature flags context and handles client-side flag fetching when needed.
The Providers component accepts initialFlags from the server, which are used to hydrate the client-side state. The preload prop controls whether the SDK should fetch flags on the client if no initial flags are provided.
"use client";
import type { ReactNode } from "react";
import type { Flag } from "@basestack/flags-js";
import { FlagsProvider } from "@basestack/flags-react/client";
import type { SDKConfig } from "@basestack/flags-js";
export const flagsConfig: SDKConfig = {
baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY,
environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY,
};
export interface ProvidersProps {
readonly children: ReactNode;
readonly initialFlags?: Flag[];
}
export function Providers({ children, initialFlags }: ProvidersProps) {
return (
<FlagsProvider
config={flagsConfig}
initialFlags={initialFlags}
preload={!initialFlags?.length}
>
{children}
</FlagsProvider>
);
}Root Layout Setup
In your root layout, fetch flags on the server using fetchFlags and pass them to the Providers component. The FlagsHydrationScript ensures flags are available immediately on the client, preventing hydration mismatches.
This approach provides the best performance because:
- Flags are fetched during server-side rendering
- No client-side loading states for initial render
- Flags are immediately available to all client components
- SEO-friendly content with flags already evaluated
import type { ReactNode } from "react";
import {
FlagsHydrationScript,
fetchFlags,
} from "@basestack/flags-react/server";
import { Providers } from "@/libs/feature-flags/providers";
export const metadata = {
title: "Basestack Flags • Next.js App Router",
};
export default async function RootLayout({
children,
}: {
children: ReactNode;
}) {
const flags = await fetchFlags({
baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY,
environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY,
});
return (
<html lang="en">
<body>
<Providers initialFlags={flags}>{children}</Providers>
<FlagsHydrationScript flags={flags} />
</body>
</html>
);
}Using Flags in Client Components
Use the useFlag hook in any client component to access feature flags. The hook returns the flag's enabled state, payload, and loading status. TypeScript generics allow you to type the payload for better type safety.
"use client";
import { useFlag } from "@basestack/flags-react/client";
export default function HomePage() {
const { enabled, payload, isLoading } = useFlag<{ variant?: string }>(
"header"
);
if (isLoading)
return <p style={{ fontFamily: "sans-serif" }}>Checking feature flags…</p>;
return (
<main style={{ fontFamily: "sans-serif", padding: "2rem" }}>
<h1>Next.js App Router Example</h1>
<p>
Flag <code>header</code> is {enabled ? "enabled" : "disabled"}
</p>
{enabled && payload ? (
<p>Variant: {JSON.stringify(payload, null, 2)}</p>
) : null}
</main>
);
}Server Actions
Server Actions allow you to fetch flags on-demand from client components. This is useful for refreshing flag values or fetching flags conditionally based on user interactions.
Create a server action that uses the server SDK to fetch flags:
"use server";
import type { Flag } from "@basestack/flags-js";
import { fetchFlag } from "@basestack/flags-react/server";
export async function getHeaderFlagAction(slug = "header"): Promise<Flag> {
return fetchFlag(slug, {
baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY,
environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY,
});
}Then use the server action in a client component with React's useTransition hook for optimal UX:
"use client";
import type { Flag } from "@basestack/flags-js";
import { useState, useTransition } from "react";
import { getHeaderFlagAction } from "./actions";
export function ServerActionDemo() {
const [flag, setFlag] = useState<Flag | null>(null);
const [isPending, startTransition] = useTransition();
const handleRefresh = () => {
startTransition(async () => {
const latest = await getHeaderFlagAction();
setFlag(latest);
});
};
return (
<section style={{ fontFamily: "sans-serif", padding: "1rem", border: "1px solid #e5e5e5" }}>
<h2>Server Function Demo</h2>
<p>
This button calls a Next.js Server Action that uses the Basestack server SDK to fetch the
<code>header</code> flag before returning the result to the client.
</p>
<button type="button" onClick={handleRefresh} disabled={isPending}>
{isPending ? "Loading flag…" : "Invoke server function"}
</button>
{flag ? (
<pre style={{ marginTop: "1rem", background: "#111", color: "#0f0", padding: "1rem" }}>
{JSON.stringify({ enabled: flag.enabled, payload: flag.payload }, null, 2)}
</pre>
) : (
<p style={{ marginTop: "1rem" }}>No flag fetched yet.</p>
)}
</section>
);
}You can also use server actions in server components directly:
import { fetchFlags } from "@basestack/flags-react/server";
import { ServerActionDemo } from "./demo";
export default async function ServerActionsPage() {
const flags = await fetchFlags({
baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY,
environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY,
});
return (
<main style={{ fontFamily: "sans-serif", padding: "2rem" }}>
<h1>Server Functions / Server Actions</h1>
<p>This page renders entirely on the server and lists the current flags.</p>
<ul>
{flags.map((flag) => (
<li key={flag.slug}>
<strong>{flag.slug}</strong>: {flag.enabled ? "enabled" : "disabled"}
</li>
))}
</ul>
<ServerActionDemo />
</main>
);
}API Route Handler
API routes in the App Router use the Route Handler pattern. You can fetch flags server-side and return them as JSON responses. This is useful for creating REST endpoints that expose flag data.
import { NextResponse } from "next/server";
import { fetchFlag } from "@basestack/flags-react/server";
import { flagsConfig } from "@/libs/feature-flags/config";
export async function GET() {
try {
const flag = await fetchFlag("header", flagsConfig);
// flag.enabled, flag.payload, etc.
return NextResponse.json({ flag });
} catch (error) {
console.error("Failed to load flag", error);
return NextResponse.json({ message: "Unable to load flag" }, { status: 500 });
}
}Pages Router Implementation
The Pages Router is Next.js's original routing system. While the App Router is recommended for new projects, the Pages Router is still fully supported and widely used.
Setup the Configuration
Create a shared configuration file that can be used in both server and client contexts:
import type { SDKConfig } from "@basestack/flags-js";
export const flagsConfig: SDKConfig = {
baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL,
projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY,
environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY,
};Setup the App Component
In _app.tsx, wrap your application with the FlagsProvider. The provider accepts initialFlags from getServerSideProps or getStaticProps, which are used to hydrate the client-side state.
The preload prop is set to true only when no initial flags are provided, ensuring flags are fetched on the client if they weren't available during SSR.
import type { Flag } from "@basestack/flags-js";
import { FlagsProvider } from "@basestack/flags-react/client";
import type { AppProps } from "next/app";
import { flagsConfig } from "@/libs/feature-flags/config";
export default function MyApp({
Component,
pageProps,
}: AppProps<{ flags?: Flag[] }>) {
const initialFlags = pageProps.flags ?? [];
return (
<FlagsProvider
config={flagsConfig}
initialFlags={initialFlags}
preload={initialFlags.length === 0}
>
<Component {...pageProps} />
</FlagsProvider>
);
}Using Flags in Pages
Fetch flags server-side using getServerSideProps or getStaticProps, then use the useFlag hook in your page component. This ensures flags are available immediately without client-side loading states.
import type { Flag } from "@basestack/flags-js";
import { useFlag } from "@basestack/flags-react/client";
import { fetchFlags } from "@basestack/flags-react/server";
import type { GetServerSideProps } from "next";
import { flagsConfig } from "@/libs/feature-flags/config";
export default function HomePage({ flags }: { flags: Flag[] }) {
// Client components can still call useFlag thanks to the provider in _app
const { enabled, payload, isLoading } = useFlag<{ color?: string }>("header");
return (
<main style={{ fontFamily: "sans-serif", padding: "2rem" }}>
<h1>Next.js Pages Router Example</h1>
<p>Server delivered {flags.length} flag(s).</p>
<p>
Flag <code>header</code> is{" "}
{isLoading ? "loading" : enabled ? "enabled" : "disabled"}
</p>
{enabled && payload ? (
<p>Payload: {JSON.stringify(payload, null, 2)}</p>
) : null}
</main>
);
}
export const getServerSideProps: GetServerSideProps<{
flags: Flag[];
}> = async () => {
const flags = await fetchFlags(flagsConfig);
return {
props: { flags },
};
};API Route
Pages Router API routes use a different pattern than App Router. Here's how to create an API endpoint that fetches flags:
import type { NextApiRequest, NextApiResponse } from "next";
import { fetchFlags } from "@basestack/flags-react/server";
import { flagsConfig } from "@/libs/feature-flags/config";
export default async function handler(
_req: NextApiRequest,
res: NextApiResponse,
) {
try {
const flags = await fetchFlags(flagsConfig);
res.status(200).json({ flags });
} catch (error) {
console.error("Failed to load flags", error);
res.status(500).json({ message: "Unable to load flags" });
}
}Understanding SSR and Hydration
When using Next.js with Basestack, flags are fetched on the server during the initial render. This provides several benefits:
- No Loading States: Flags are available immediately when components render
- SEO Friendly: Search engines can see the correct content based on flag states
- Performance: Reduces client-side API calls and improves Time to Interactive (TTI)
- Consistency: Server and client render the same content, preventing hydration mismatches
The FlagsHydrationScript (App Router) and initialFlags prop ensure that the client receives the same flag values that were used during server-side rendering.
Best Practices
1. Fetch Flags at the Root Level
Fetch flags in your root layout (app/layout.tsx) or _app.tsx to ensure they're available throughout your application. This prevents multiple API calls and ensures consistent flag values.
2. Use TypeScript Generics for Payloads
Type your flag payloads for better type safety and developer experience:
const { enabled, payload } = useFlag<{ variant: string; color: string }>("header");3. Handle Loading States
While flags are typically available immediately due to SSR, handle loading states for better UX:
const { enabled, isLoading } = useFlag("header");
if (isLoading) return <LoadingSpinner />;4. Error Handling
Always handle potential errors when fetching flags:
try {
const flags = await fetchFlags(flagsConfig);
} catch (error) {
console.error("Failed to fetch flags", error);
// Fallback to default behavior
}5. Cache Considerations
The SDK includes built-in caching. Flags are cached for 5 minutes by default, reducing unnecessary API calls. You can configure cache settings in the SDK config if needed.
Troubleshooting
Flags Not Updating
If flags aren't updating as expected:
- Check Cache: Flags are cached for 5 minutes. Wait for the cache to expire or clear it programmatically
- Verify Environment Variables: Ensure
NEXT_PUBLIC_prefixed variables are set correctly - Check Network: Verify your application can reach the Basestack API endpoint
Hydration Mismatches
If you see hydration warnings:
- Ensure
FlagsHydrationScriptis included: In App Router, make sureFlagsHydrationScriptis in your root layout - Check
initialFlagsprop: Ensure flags fetched server-side are passed to the provider - Avoid Client-Only Logic: Don't conditionally render different content based on flags that differ between server and client
Components Rendering Twice
If your components are rendering twice, this is likely due to React.StrictMode in development. This is expected behavior and helps identify potential issues. In production, components will only render once.
Environment Variables Not Available
If environment variables aren't available:
- Check Prefix: Use
NEXT_PUBLIC_prefix for variables needed in client components - Restart Dev Server: After adding environment variables, restart your Next.js dev server
- Verify
.envFile: Ensure your.envfile is in the project root
API Reference - Props & Parameters
Components
FlagsProvider
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
config | SDKConfig | Yes | - | Configuration object for the Flags SDK |
children | ReactNode | Yes | - | React children to be wrapped by the provider |
initialFlags | Flag[] | No | [] | Initial flags to populate the provider with |
preload | boolean | No | true | Whether to automatically preload flags on mount |
onError | (error: Error) => void | No | - | Callback function called when an error occurs |
FlagsHydrationScript
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
flags | Flag[] | Yes | - | Array of flags to hydrate on the client |
id | string | No | "basestack-flags-hydration" | HTML id attribute for the script tag |
nonce | string | No | - | Content Security Policy nonce for the script tag |
globalKey | string | No | "__BASESTACK_FLAGS__" | Global window key to store hydrated flags |
Hooks
useFlag<TPayload>(slug, options?)
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
slug | string | Yes | - | The flag slug to retrieve |
options | UseFlagOptions<TPayload> | No | - | Options object (see below) |
Options (UseFlagOptions):
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
defaultEnabled | boolean | No | false | Default enabled state if flag is not loaded |
defaultPayload | TPayload | No | - | Default payload value if flag is not loaded |
fetch | boolean | No | true | Whether to automatically fetch the flag if not cached |
Returns (UseFlagResult<TPayload>):
| Property | Type | Description |
|---|---|---|
flag | Flag | undefined | The flag object if available |
enabled | boolean | Whether the flag is enabled |
payload | TPayload | undefined | The flag payload |
isLoading | boolean | Whether the flag is currently loading |
error | Error | null | Any error that occurred during fetching |
refresh | () => Promise<Flag | undefined> | Function to manually refresh the flag |
useFlags()
Parameters: None
Returns:
| Property | Type | Description |
|---|---|---|
flags | Flag[] | Array of all flags |
flagsBySlug | Record<string, Flag> | Object mapping flag slugs to flag objects |
isLoading | boolean | Whether flags are currently loading |
error | Error | null | Any error that occurred during fetching |
refresh | () => Promise<void> | Function to manually refresh all flags |
useFlagsClient()
Parameters: None
Returns:
| Property | Type | Description |
|---|---|---|
| (client) | FlagsSDK | The Flags SDK client instance |
useFlagsContext()
Parameters: None
Returns:
| Property | Type | Description |
|---|---|---|
client | FlagsSDK | The Flags SDK client instance |
flags | Map<string, Flag> | Map of flags by slug |
loading | boolean | Whether flags are currently loading |
error | Error | null | Any error that occurred |
refresh | () => Promise<void> | Function to manually refresh all flags |
upsertFlag | (flag: Flag) => void | Function to update or insert a flag |
Server Functions
createServerFlagsClient(config)
| Parameter | Type | Required | Description |
|---|---|---|---|
config | SDKConfig | Yes | Configuration object for the Flags SDK |
Returns: FlagsSDK - A new Flags SDK client instance
fetchFlag(slug, config)
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | The flag slug to fetch |
config | SDKConfig | Yes | Configuration object for the Flags SDK |
Returns: Promise<Flag> - The fetched flag
fetchFlags(config, slugs?)
| Parameter | Type | Required | Description |
|---|---|---|---|
config | SDKConfig | Yes | Configuration object for the Flags SDK |
slugs | string[] | No | Optional array of flag slugs to fetch. If not provided, fetches all flags |
Returns: Promise<Flag[]> - Array of fetched flags
Utility Functions
readHydratedFlags(globalKey?)
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
globalKey | string | No | "__BASESTACK_FLAGS__" | Global window key to read hydrated flags from |
Returns: Flag[] | undefined - Array of hydrated flags, or undefined if not available (e.g., on server)
Types
SDKConfig
Exported from @basestack/flags-js - Configuration for the Flags SDK
Flag
Exported from @basestack/flags-js - Flag object type
CacheConfig
Exported from @basestack/flags-js - Cache configuration type