Browser SDKs

React SDK

Install and configure the React SDK provider, and evaluate configs and feature flags in React components

Introduction

The React SDK is intended to be used by React web applications running on modern web browsers. All modern web browsers on popular platforms should be supported.

The minimum supported version of React is 16.8.0.

Browser SDKs are tested on the latest versions of Chrome, Firefox, Safari, and Edge.

Telemetry collection within browser SDKs 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/react-web-sdk

npm install --save @configdirector/react-web-sdk

Configure and initialize the provider

Initialize the provider with your client SDK key:

main.tsx
import { createRoot } from "react-dom/client"
import App from "./App"
import { ConfigDirectorProvider } from "@configdirector/react-web-sdk";

createRoot(document.getElementById("root")!).render(
  <ConfigDirectorProvider sdkKey="YOUR-CLIENT-SDK-KEY">
    <App />
  </ConfigDirectorProvider>
);

Alternatively, you can initialize it with the HOC asynchronously and wait for initialization before starting your application. This can prevent the config values to flicker from the default value to the evaluated value during initialization.

An optional timeout can be provided to configure how long to await for the client to initialize before rendering the application. In the event of a timeout, the client will continue to attempt to connect in the background and config values will return the default value provided in code until a connection to ConfigDirector services is established.

main.tsx
import { createRoot } from "react-dom/client"
import App from "./App"
import { withProvider } from "@configdirector/react-web-sdk";

(async () => {
  const ConfigDirectorProvider = await withProvider({
    sdkKey: "YOUR-CLIENT-SDK-KEY",
    timeout: { 2_000 }, // 2,000 milliseconds timeout, defaults to 3,000
  });

  createRoot(document.getElementById('root')!).render(
    <ConfigDirectorProvider>
      <App />
    </ConfigDirectorProvider>
  );
})();

Additional configuration options

These configuration options can be passed in to the ConfigDirectorProvider or to the withProvider HOC as an object in the second argument.

context

The user context to be used during targeting rules evaluation:

main.tsx
import { createRoot } from "react-dom/client";
import App from "./App";
import { ConfigDirectorProvider } from "@configdirector/react-web-sdk";

createRoot(document.getElementById("root")!).render(
  <ConfigDirectorProvider
    sdkKey="YOUR-CLIENT-SDK-KEY"
    context={{ id: "12345", name: "Example User", traits: { region: "North America" } }}>
    <App />
  </ConfigDirectorProvider>,
);

logger

By default, the SDK 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:

main.tsx
import { createRoot } from "react-dom/client";
import App from "./App";
import { ConfigDirectorProvider, createConsoleLogger } from "@configdirector/react-web-sdk";

createRoot(document.getElementById("root")!).render(
  <ConfigDirectorProvider sdkKey="YOUR-CLIENT-SDK-KEY" logger={createConsoleLogger("debug")}>
    <App />
  </ConfigDirectorProvider>,
);

Implement your own logger adapter:

main.tsx
import { createRoot } from "react-dom/client";
import App from "./App";
import { ConfigDirectorProvider, ConfigDirectorLogger } from "@configdirector/react-web-sdk";

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
  },
};

createRoot(document.getElementById("root")!).render(
  <ConfigDirectorProvider sdkKey="YOUR-CLIENT-SDK-KEY" logger={myLogger}>
    <App />
  </ConfigDirectorProvider>,
);

appName and appVersion

These 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.

main.tsx
import { createRoot } from "react-dom/client";
import App from "./App";
import { ConfigDirectorProvider } from "@configdirector/react-web-sdk";

createRoot(document.getElementById("root")!).render(
  <ConfigDirectorProvider sdkKey="YOUR-CLIENT-SDK-KEY" appName="YOUR-APP-NAME" appVersion="1.0.2">
    <App />
  </ConfigDirectorProvider>,
);

timeout

The timeout, in milliseconds, to be used in initialization and when updating the context. This is how long the withProvider HOC will wait for data from ConfigDirector services before resolving its Promise. If the timeout is reached, withProvider will return but the client will still be in an unready status and returning default values. The client will continue to attempt to connect and retrieve config values in the background.

When using the ConfigDirectorProvider, the timeout is not as relevant since initialization will happen asynchronously.

main.tsx
import { createRoot } from "react-dom/client"
import App from "./App"
import { withProvider } from "@configdirector/react-web-sdk";

(async () => {
  const ConfigDirectorProvider = await withProvider({
    sdkKey: "YOUR-CLIENT-SDK-KEY",
    timeout: 2_000, // 2,000 milliseconds timeout, defaults to 3,000
  });

  createRoot(document.getElementById('root')!).render(
    <ConfigDirectorProvider>
      <App />
    </ConfigDirectorProvider>
  );
})();

mode

The connection mode, 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. Defaults to streaming.

pollingInterval

The interval, in seconds, between requests for config updates when mode is polling. It has no effect in streaming mode. A value below the minimum of 30 seconds is raised to 30 seconds and a warning is logged. Defaults to 60 seconds. See Polling intervals.

main.tsx
import { createRoot } from "react-dom/client";
import App from "./App";
import { ConfigDirectorProvider } from "@configdirector/react-web-sdk";

createRoot(document.getElementById("root")!).render(
  <ConfigDirectorProvider sdkKey="YOUR-CLIENT-SDK-KEY" mode="polling" pollingInterval={120}>
    <App />
  </ConfigDirectorProvider>,
);

url

The base URL used to connect to ConfigDirector services. This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services.

Retrieve config values

Retrieve a config value with the useConfigValue hook:

YourComponent.tsx
import { useConfigValue } from "@configdirector/react-web-sdk";

function YourComponent() {
  const { value } = useConfigValue("my-config-key", false);

  return (<p>my-config-key: {value}</p>);
}

export default YourComponent;

You can also determine if the client is still initializing. This can be useful in the event of a slow connection where retrieving the initial config values may be slow, so rather than transition from the in-code default value to the evaluated value, you can show a loading state until the client is ready and config values are evaluated:

YourComponent.tsx
import { useConfigValue } from "@configdirector/react-web-sdk";

function YourComponent() {
  const { value, loading } = useConfigValue("my-config", "default value");

  if (loading) {
    return <p>Loading...</p>;
  } else {
    return <p>My config value: {value}</p>;
  }
}

export default YourComponent;

Update the user context

A user context can be provided when initializing the provider:

main.tsx
import { createRoot } from "react-dom/client"
import App from "./App"
import { ConfigDirectorProvider } from "@configdirector/react-web-sdk";

createRoot(document.getElementById("root")!).render(
  <ConfigDirectorProvider
    sdkKey="YOUR-CLIENT-SDK-KEY"
    context={{ id: "12345", name: "Example User", traits: { region: "North America" } }}>
    <App />
  </ConfigDirectorProvider>
);

The user context can also be updated with the useContext hook:

YourComponent.tsx
import { useConfigValue, useContext } from "@configdirector/react-web-sdk";

function YourComponent() {
  const { value: myConfigValue } = useConfigValue("my-config", "default value");
  const { updateContext } = useContext();

  useEffect(() => {
    updateContext({
      id: "654321",
      name: "Another User",
      traits: {
        region: "Australia",
      },
    });
  }, []);

  return (<p>My config value: {myConfigValue}</p>);
}

export default YourComponent;
In client SDKs (browser and mobile), updating the user context re-establishes a new connection to ConfigDirector servers with the new context. While the new connection is in flight, config values will continue to evaluate to the currently cached values from the prior user context.

useClient hook

In the event that you need to have access to the underlying Javascript ConfigDirectorClient instance for more complex behaviors, you can access the instance via the useClient hook:

YourComponent.tsx
import { useClient } from "@configdirector/react-web-sdk";

function YourComponent() {
  const { client } = useClient();

  // Utilize the client for more involved logic

  return <p>A more complex component</p>;
}

export default YourComponent;
Proceed with caution when using the useClient hook. The useConfigValue and useContext hooks manage listeners and cleanup automatically. However, if you make use of the useClient hook, you must manage cleaning up any listeners yourself.
Additionally, any calls to dispose, unwatch, or unwatchAll on the client instance can have unintended side effects and may result in subtle bugs.

Test your components

Use the SDK's testing entry point, @configdirector/react-web-sdk/testing, to test components that read your configs and flags. It 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, including ConfigDirectorProvider and the hooks, so your components behave exactly as they do against ConfigDirector, and no network connection is opened and no telemetry is sent.

Render the production ConfigDirectorProvider with its client prop:

Checkout.test.tsx
import { act, render, screen } from "@testing-library/react";
import { ConfigDirectorProvider } from "@configdirector/react-web-sdk";
import { createTestClient } from "@configdirector/react-web-sdk/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();
});

The provider renders its children before initialization completes, so the first assertion uses findBy… rather than getBy…. Wrap setValue, removeValue, and replaceValues in act(), since they re-render the components under the provider at once.

Components that render their own provider

A component that contains the production provider, such as an App that renders <ConfigDirectorProvider sdkKey="…">, would build a real client. installTestClient makes every provider mounted without a client prop use the test client instead:

App.test.tsx
import { createTestClient, installTestClient } from "@configdirector/react-web-sdk/testing";

let testClient: TestClient;
let uninstallTestClient: () => void;

beforeEach(() => {
  testClient = createTestClient({ values: { "new-checkout": true } });
  uninstallTestClient = installTestClient(testClient);
});
afterEach(() => uninstallTestClient());

test("renders the app with the flag on", async () => {
  render(<App />);
  expect(await screen.findByText("New checkout")).toBeInTheDocument();
});

While installed, the provider's construction props (sdkKey, url, mode, timeout, pollingInterval, logger, appName, appVersion) are ignored; context and hooks apply to the installed client as they do with a client prop. withProvider also uses the installed client. A provider reads the installed client when it is constructed, so install before rendering. Installing again replaces the installed test client, and each uninstall function restores the one installed before it.

What the provider does with a given client

  • It never disposes a client it was given, whether through the client prop or installTestClient. It disposes only a client it built from sdkKey.
  • It does not call initialize on a client that is ready or initializing. If the client is ready and the context prop is given and differs from the client's context, it calls updateContext. A provider given a ready client reports the ready status on its first render.
  • hooks given with a client are registered on it while the provider is mounted and removed when it unmounts, like every other handler the provider registers.
  • The context prop is compared by deep equality, on mount and on later renders, so a new object with the same contents does not reconnect.
  • A client prop that changes after the provider mounted is ignored with a warning. Remount the provider, for example with a new key, to use another client.

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, so loading and error states can be tested, and holdContextUpdate, completeContextUpdate, and failContextUpdate do the same for updateContext. contextUpdates records the context of every initialize and updateContext call. The JavaScript SDK's testing section describes every control and what to expect from the client.