Next.js SDK
Introduction
The Next.js SDK combines a client and a server SDK to provide configs to the browser, to server routes, and to SSR. It is intended to be used by Next.js web applications running on modern web browsers. All modern web browsers on popular platforms should be supported.
The minimum supported version of Next.js is 14.0.
Browser SDKs are tested on the latest versions of Chrome, Firefox, Safari, and Edge.
Telemetry collection within the browser uses Web Workers to aggregate and send telemetry data. If the SDK runs on older browsers without Web Workers support, config evaluation will continue to work but no telemetry data will be collected for that session.
Installation
The SDK can be installed from NPM: https://www.npmjs.com/package/@configdirector/nextjs-sdk
npm install --save @configdirector/nextjs-sdk
yarn add @configdirector/nextjs-sdk
pnpm add @configdirector/nextjs-sdk
bun add @configdirector/nextjs-sdk
Configure the SDK
Set up the server instance to initialize at startup
The Next.js SDK maintains a singleton server SDK instance used in server routes and server side rendering (SSR). In order to register this instance at startup, you can make use of the Next.js instrumentation.ts file:
export async function register() {
const { register } = await import("@configdirector/nextjs-sdk/server");
await register({
// The server SDK key is a secret value, do not commit it to your source code repository.
// In this example, we are assuming the environment variable CONFIGDIRECTOR_SERVER_SDK_KEY
// will be populated at runtime with your server SDK key in the server environment
serverSdkKey: process.env["CONFIGDIRECTOR_SERVER_SDK_KEY"]!,
});
}
If you are using a version of Next.js older than 15, instrumentation is an experimental feature that must be enabled explicitly:
module.exports = {
experimental: {
instrumentationHook: true,
},
};
Configure the client provider
The ConfigDirector hooks are only available within a ConfigDirectorProvider. The simplest approach is to wrap your application in a ConfigDirectorProvider in your layout. There are a couple of options for this.
Using a dynamic layout and a server-side environment variable for the client key:
import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/server";
import type { ReactNode } from "react";
export const dynamic = "force-dynamic";
export default async function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<ConfigDirectorProvider sdkKey={process.env["CONFIGDIRECTOR_CLIENT_SDK_KEY"]!}>
<main>{children}</main>
</ConfigDirectorProvider>
</body>
</html>
);
}
force-dynamic makes the layout read the environment variable on every request, so one build can be deployed to several environments. The trade-off is that the pages lose static generation. If you build once per environment instead, you can leave force-dynamic out: the environment variable set at build time is baked into that build's static output.
NEXT_PUBLIC_ environment variable for the SDK key, even in a static layout. Next.js replaces every NEXT_PUBLIC_ reference with its build-time value, which freezes the key into the build and exposes it to all browser code. A plain environment variable reaches the browser only through the provider.Additionally, you can configure the provider with a user context. In this example, we retrieve the userId from a cookie:
import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/server";
import type { ReactNode } from "react";
import { cookies } from "next/headers";
export const dynamic = "force-dynamic";
export default async function RootLayout({ children }: { children: ReactNode }) {
const userId = (await cookies()).get("userId")?.value;
return (
<html lang="en">
<body>
<ConfigDirectorProvider
sdkKey={process.env["CONFIGDIRECTOR_CLIENT_SDK_KEY"]!}
context={{ id: userId }}>
<main>{children}</main>
</ConfigDirectorProvider>
</body>
</html>
);
}
Additional configuration options
appName and appVersion
These metadata options allow you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application.
export async function register() {
const { register } = await import("@configdirector/nextjs-sdk/server");
await register({
serverSdkKey: process.env["CONFIGDIRECTOR_SERVER_SDK_KEY"]!,
metadata: {
appName: "YOUR-APP-NAME",
appVersion: "1.0.2",
},
});
}
The appName and appVersion are automatically propagated to the client SDK instance via the ConfigDirectorProvider RSC. However, if needed they can be overridden to a different value when configuring the provider as well:
import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/server";
import type { ReactNode } from "react";
export default async function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<ConfigDirectorProvider
sdkKey={process.env["CONFIGDIRECTOR_CLIENT_SDK_KEY"]!}
appName="A-DIFFERENT-APP-NAME"
appVersion="1.0.3">
<main>{children}</main>
</ConfigDirectorProvider>
</body>
</html>
);
}
logger (server only)
By default, the SDK on the server side logs to the console and it is set to log warnings and errors only. You can configure a logger by either creating a ConfigDirector console logger with a different log level, or by implementing the ConfigDirectorLogger interface to provide your own logger. The interface can be used to create an adapter to another logging library.
Configure the ConfigDirector console logger to a different level:
export async function register() {
const { register, createConsoleLogger } = await import("@configdirector/nextjs-sdk/server");
await register({
serverSdkKey: process.env["CONFIGDIRECTOR_SERVER_SDK_KEY"]!,
logger: createConsoleLogger("info"),
});
}
Implement your own logger adapter:
import type { ConfigDirectorLogger } from "@configdirector/nextjs-sdk/server";
export async function register() {
const { register } = await import("@configdirector/nextjs-sdk/server");
const myLogger: ConfigDirectorLogger = {
debug: function (message: string, ...args: any): void {
// your specific logging library implementation here
},
info: function (message: string, ...args: any): void {
// your specific logging library implementation here
},
warn: function (message: string, ...args: any): void {
// your specific logging library implementation here
},
error: function (message: string, ...args: any): void {
// your specific logging library implementation here
},
};
await register({
serverSdkKey: process.env["CONFIGDIRECTOR_SERVER_SDK_KEY"]!,
logger: myLogger,
});
}
logLevel (client only)
The SDK on the client side logs to the browser console. You can adjust the ConfigDirector's SDK logging level for the client side via the logLevel option (defaults to warn):
import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/server";
import type { ReactNode } from "react";
export default async function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<ConfigDirectorProvider
sdkKey={process.env["CONFIGDIRECTOR_CLIENT_SDK_KEY"]!}
logLevel="info">
<main>{children}</main>
</ConfigDirectorProvider>
</body>
</html>
);
}
mode and pollingInterval (client only)
The connection mode of the client-side provider, which can be either streaming or polling. It is recommended to use the default of streaming unless you have a specific need to use polling instead. pollingInterval is the interval, in seconds, between requests for config updates when mode is polling, and has no effect in streaming mode. It defaults to 60 seconds, and a value below the minimum of 30 seconds is raised to 30 seconds and a warning is logged. See Polling intervals.
import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/server";
import type { ReactNode } from "react";
export default async function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<ConfigDirectorProvider
sdkKey={process.env["CONFIGDIRECTOR_CLIENT_SDK_KEY"]!}
mode="polling"
pollingInterval={120}>
<main>{children}</main>
</ConfigDirectorProvider>
</body>
</html>
);
}
Additional server configuration
The register function accepts the same configuration values as the Node.js server SDK. You can refer to the Node.js SDK for a full list of those configuration options. The server instance uses the server SDK polling defaults: connection.pollingInterval defaults to 300 seconds with a minimum of 60 seconds.
Retrieve config values
In your client components, retrieve a config value with the useConfigValue hook. On the server side, getConfigClient returns the global server instance of the client (an instance of the Node.js SDK).
Retrieving config values in client components:
"use client";
import { useConfigValue } from "@configdirector/nextjs-sdk/client";
function YourComponent() {
const { value } = useConfigValue("my-config-key", false);
return (<p>my-config-key: {value}</p>);
}
export default YourComponent;
Retrieving config values in server routes:
import { getConfigClient } from "@configdirector/nextjs-sdk/server";
export async function GET() {
const client = getConfigClient();
const value = client.getValue("my-config-key", false);
return Response.json({ "my-config-key": value });
}
SSR config evaluation
Evaluation of config values during server side rendering (SSR) utilizes the global server instance of the ConfigDirector Node.js client. That means there are no additional network requests that take place since the server SDK evaluates targeting rules locally for any given user context.
Update the user context
The user context can be updated with the useContext hook:
"use client";
import { useEffect } from "react";
import { useConfigValue, useContext } from "@configdirector/nextjs-sdk/client";
function YourComponent() {
const { value } = useConfigValue("my-config-key", false);
const { updateContext } = useContext();
useEffect(() => {
updateContext({
id: "654321",
name: "Another User",
traits: {
region: "Australia",
},
});
}, []);
return <p>my-config-key: {value}</p>;
}
export default YourComponent;
On server routes, the user context is provided at evaluation time as the third argument to getValue. Unlike client SDKs, the server SDKs are able to evaluate targeting rules for the given user context locally without additional network calls.
import { getConfigClient } from "@configdirector/nextjs-sdk/server";
export async function GET() {
const client = getConfigClient();
const value = client.getValue("my-config-key", false, {
id: "654321",
name: "Another User",
traits: {
region: "Australia",
},
});
return Response.json({ "my-config-key": value });
}
Test your code
The SDK ships two testing entry points. @configdirector/nextjs-sdk/client/testing is for Client Components that read your configs and flags, and @configdirector/nextjs-sdk/server/testing is for server code that uses getConfigClient(), generateSsrConfigSet(), or the server ConfigDirectorProvider. Each creates a test client: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector is replaced. Everything else is the same code that runs in production, so the code under test behaves exactly as it does against ConfigDirector, and no network connection is opened and no telemetry is sent.
Client Components
Render the production client ConfigDirectorProvider with its client prop:
import { act, render, screen } from "@testing-library/react";
import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/client";
import { createTestClient } from "@configdirector/nextjs-sdk/client/testing";
import { Checkout } from "./Checkout";
test("shows the new checkout when the flag is on", async () => {
const testClient = createTestClient({ values: { "new-checkout": true } });
render(
<ConfigDirectorProvider client={testClient.client}>
<Checkout />
</ConfigDirectorProvider>,
);
expect(await screen.findByText("New checkout")).toBeInTheDocument();
act(() => testClient.setValue("new-checkout", false));
expect(screen.getByText("Classic checkout")).toBeInTheDocument();
});
Until the client is ready, the hooks show initialConfigs when the provider was given them, or the in-code default value, so the first assertion uses findBy… rather than getBy…; a test client whose client was initialized before rendering is ready on the first render. Wrap setValue, removeValue, and replaceValues in act().
For a component that renders the client provider itself with an sdkKey, installTestClient makes every provider mounted without a client prop use the test client instead, until the returned function uninstalls it:
import { createTestClient, installTestClient } from "@configdirector/nextjs-sdk/client/testing";
let uninstallTestClient: () => void;
beforeEach(() => {
uninstallTestClient = installTestClient(createTestClient({ values: { "new-checkout": true } }));
});
afterEach(() => uninstallTestClient());
While installed, the provider's construction props (sdkKey, url, mode, timeout, pollingInterval, logLevel, appName, appVersion) are ignored; context, hooks, and initialConfigs apply as they do with a client prop. A provider given a client never disposes it, removes its handlers when it unmounts, does not call initialize on a client that is ready or initializing, updates the context only when the context prop is given and differs from the client's context, compares contexts by deep equality, and ignores a client prop that changes after mount with a warning.
Server code
Server code reads the singleton that register() creates in instrumentation.ts through getConfigClient(). register() never runs in unit tests, so installServerTestClient puts a server test client's client in its place and returns a function that restores the previous one. It does not initialize the client; the test does, as register() would:
import { getConfigClient } from "@configdirector/nextjs-sdk/server";
import { createTestClient, installServerTestClient } from "@configdirector/nextjs-sdk/server/testing";
let uninstallServerTestClient: () => void;
beforeEach(async () => {
const testClient = createTestClient({ values: { "new-checkout": true } });
uninstallServerTestClient = installServerTestClient(testClient);
await testClient.client.initialize();
});
afterEach(() => uninstallServerTestClient());
test("serves the flag to every user", () => {
expect(getConfigClient().getValue("new-checkout", false, { id: "user-1" })).toBe(true);
});
generateSsrConfigSet() and the server ConfigDirectorProvider then serve the test client's values. The server provider is an async Server Component, which React Testing Library cannot render; call it as an async function and assert on the initialConfigs prop of the element it returns. Every value is served as an unconditional config, so every context receives the same value.
Test client controls
values, setValue, removeValue, and replaceValues take native values, and the config type follows from each value: a boolean, an integral number (integer), any other number (float), a string, or a plain object or array (json). holdInitialization, completeInitialization, and failInitialization drive initialize; the client test client also holds, completes, or fails updateContext and records every context in contextUpdates. The JavaScript SDK's testing section and the Node.js SDK's testing section describe every control and what to expect from each client.