Skip to main content
Back to Blog
Tutorials
2 min read
December 13, 2024

How to Implement Feature Toggles with LaunchDarkly in Next.js

Integrate LaunchDarkly feature flags in Next.js for server and client components, with targeting rules and A/B testing.

Ryel Banfield

Founder & Lead Developer

Feature flags let you decouple deployment from release. You can ship code behind flags and enable features gradually.

Install the SDK

pnpm add @launchdarkly/node-server-sdk launchdarkly-react-client-sdk

Server-Side Client

// lib/launchdarkly/server.ts
import * as LaunchDarkly from "@launchdarkly/node-server-sdk";

let client: LaunchDarkly.LDClient | null = null;

export async function getLDClient(): Promise<LaunchDarkly.LDClient> {
  if (client) return client;

  client = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY!);
  await client.waitForInitialization();
  return client;
}

export interface LDUser {
  key: string;
  email?: string;
  name?: string;
  custom?: Record<string, string | number | boolean>;
}

export async function getFlag<T>(
  flagKey: string,
  user: LDUser,
  defaultValue: T
): Promise<T> {
  const ld = await getLDClient();
  const context: LaunchDarkly.LDContext = {
    kind: "user",
    key: user.key,
    email: user.email,
    name: user.name,
    ...user.custom,
  };
  return ld.variation(flagKey, context, defaultValue) as Promise<T>;
}

export async function getAllFlags(user: LDUser): Promise<Record<string, unknown>> {
  const ld = await getLDClient();
  const context: LaunchDarkly.LDContext = {
    kind: "user",
    key: user.key,
    email: user.email,
    name: user.name,
  };
  const allFlags = await ld.allFlagsState(context);
  return allFlags.toJSON();
}

Server Component Usage

// app/(site)/pricing/page.tsx
import { getFlag } from "@/lib/launchdarkly/server";
import { auth } from "@/lib/auth";
import { PricingTable } from "@/components/PricingTable";
import { NewPricingTable } from "@/components/NewPricingTable";

export default async function PricingPage() {
  const session = await auth();
  const user = {
    key: session?.user?.id ?? "anonymous",
    email: session?.user?.email ?? undefined,
  };

  const showNewPricing = await getFlag<boolean>("new-pricing-page", user, false);
  const discountPercentage = await getFlag<number>("discount-percentage", user, 0);

  return (
    <div className="max-w-5xl mx-auto py-16 px-4">
      {showNewPricing ? (
        <NewPricingTable discount={discountPercentage} />
      ) : (
        <PricingTable />
      )}
    </div>
  );
}

Client Provider

// components/providers/FeatureFlagProvider.tsx
"use client";

import { withLDProvider } from "launchdarkly-react-client-sdk";

function InnerProvider({ children }: { children: React.ReactNode }) {
  return <>{children}</>;
}

export const FeatureFlagProvider = withLDProvider({
  clientSideID: process.env.NEXT_PUBLIC_LD_CLIENT_ID!,
  reactOptions: {
    useCamelCaseFlagKeys: true,
  },
  context: {
    kind: "user",
    key: "anonymous",
  },
})(InnerProvider);

Client Component Usage

"use client";

import { useFlags, useLDClient } from "launchdarkly-react-client-sdk";
import { useEffect } from "react";

interface FeatureFlags {
  showBetaBanner: boolean;
  maxUploadSize: number;
  enableDarkMode: boolean;
}

export function BetaBanner() {
  const flags = useFlags() as FeatureFlags;

  if (!flags.showBetaBanner) return null;

  return (
    <div className="bg-blue-50 border-b border-blue-200 py-2 text-center text-sm text-blue-800">
      You are using a beta feature. Share feedback in our community.
    </div>
  );
}

export function useIdentifyUser(user: { id: string; email: string; plan: string }) {
  const ldClient = useLDClient();

  useEffect(() => {
    if (!ldClient || !user.id) return;

    ldClient.identify({
      kind: "user",
      key: user.id,
      email: user.email,
      plan: user.plan,
    });
  }, [ldClient, user.id, user.email, user.plan]);
}

Bootstrap Flags for SSR

Pass server-evaluated flags to the client to avoid flicker:

// app/layout.tsx
import { getAllFlags } from "@/lib/launchdarkly/server";
import { FeatureFlagProvider } from "@/components/providers/FeatureFlagProvider";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const flags = await getAllFlags({ key: "anonymous" });

  return (
    <html lang="en">
      <body>
        <FeatureFlagProvider
          options={{
            bootstrap: flags,
          }}
        >
          {children}
        </FeatureFlagProvider>
      </body>
    </html>
  );
}

Feature Flag Hook with Fallback

"use client";

import { useFlags } from "launchdarkly-react-client-sdk";

export function useFeatureFlag<T>(key: string, defaultValue: T): T {
  const flags = useFlags();

  // Handle SSR and loading states
  if (typeof flags !== "object" || flags === null) {
    return defaultValue;
  }

  const camelKey = key.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
  return (flags as Record<string, unknown>)[camelKey] as T ?? defaultValue;
}

Gradual Rollout Pattern

export function FeatureGate({
  flag,
  children,
  fallback = null,
}: {
  flag: string;
  children: React.ReactNode;
  fallback?: React.ReactNode;
}) {
  const enabled = useFeatureFlag(flag, false);
  return enabled ? <>{children}</> : <>{fallback}</>;
}

// Usage
function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <FeatureGate flag="new-analytics-widget">
        <AnalyticsWidget />
      </FeatureGate>
      <FeatureGate
        flag="ai-assistant"
        fallback={<p className="text-sm text-muted-foreground">Coming soon</p>}
      >
        <AIAssistant />
      </FeatureGate>
    </div>
  );
}

Need Gradual Feature Rollouts?

We help teams implement feature flagging and progressive delivery systems. Contact us to learn more.

feature flagsLaunchDarklyNext.jsdeploymenttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles