Mobile SDKs

React Native SDK

Install and configure the React Native SDK provider, and evaluate configs and feature flags in React Native and Expo apps

Introduction

The React Native SDK is intended to be used by React Native mobile applications.

The minimum supported version of React Native is 0.75, and the minimum supported version of Expo is 52.

Installation

The SDK can be installed from NPM: https://www.npmjs.com/package/@configdirector/react-native-sdk

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

Configure and initialize the provider

Initialize the provider with your client SDK key:

main.tsx
import { View } from "react-native";
import AwesomeApp from "./AwesomeApp";
import { ConfigDirectorProvider } from "@configdirector/react-native-sdk";

export default function App() {
  return (
    <ConfigDirectorProvider
      sdkKey={"YOUR-CLIENT-SDK-KEY"}
      appName="MyAwesomeApp"
      appVersion="1.0.0">
      <View>
        <AwesomeApp />
      </View>
    </ConfigDirectorProvider>
  );
};

Additional configuration options

These configuration options can be passed in to the ConfigDirectorProvider.

appName and appVersion

The appName and appVersion props 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.

React Native itself does not expose the app name or version, so whichever of the two you leave unset is read from expo-application or react-native-device-info when your app has either installed. Without one of those modules, set both props if you intend to target on them; the SDK says which one it could not find at the info log level.

main.tsx
import { View } from "react-native";
import AwesomeApp from "./AwesomeApp";
import { ConfigDirectorProvider } from "@configdirector/react-native-sdk";

export default function App() {
  return (
    <ConfigDirectorProvider
      sdkKey={"YOUR-CLIENT-SDK-KEY"}
      appName="MyAwesomeApp"
      appVersion="1.0.0">
      <View>
        <AwesomeApp />
      </View>
    </ConfigDirectorProvider>
  );
};

context

The user context to be used during targeting rules evaluation:

main.tsx
import { View } from "react-native";
import AwesomeApp from "./AwesomeApp";
import { ConfigDirectorProvider } from "@configdirector/react-native-sdk";

export default function App() {
  return (
    <ConfigDirectorProvider
      sdkKey={"YOUR-CLIENT-SDK-KEY"}
      appName="MyAwesomeApp"
      appVersion="1.0.0"
      context={{ id: "12345", name: "Example User", traits: { region: "North America" } }}>
      <View>
        <AwesomeApp />
      </View>
    </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 { View } from "react-native";
import AwesomeApp from "./AwesomeApp";
import { ConfigDirectorProvider, createConsoleLogger } from "@configdirector/react-native-sdk";

export default function App() {
  return (
    <ConfigDirectorProvider
      sdkKey={"YOUR-CLIENT-SDK-KEY"}
      appName="MyAwesomeApp"
      appVersion="1.0.0"
      logger={createConsoleLogger("debug")}>
      <View>
        <AwesomeApp />
      </View>
    </ConfigDirectorProvider>
  );
}

Implement your own logger adapter:

main.tsx
import { View } from "react-native";
import AwesomeApp from "./AwesomeApp";
import { ConfigDirectorProvider, ConfigDirectorLogger } from "@configdirector/react-native-sdk";

export default function App() {
  return (
    <ConfigDirectorProvider
      sdkKey={"YOUR-CLIENT-SDK-KEY"}
      appName="MyAwesomeApp"
      appVersion="1.0.0"
      logger={myLogger}>
      <View>
        <AwesomeApp />
      </View>
    </ConfigDirectorProvider>
  );
}

netInfoSubscribe

Allows providing a subscription function with the same signature as NetInfo.addEventListener from @react-native-community/netinfo. This options can be used to enable immediate reconnection when the device regains network connectivity instead of waiting for the next exponential-backoff retry.

For example, if a user lost network connectivity for several minutes, once they regain network connectivity there may still be a delay of minutes for the exponential backoff retry to catch up. In most cases, this is not needed and the standard retry with exponential backoff is sufficient. However, if your use case needs immediate reconnection, this can be a useful option.

main.tsx
import { View } from "react-native";
import AwesomeApp from "./AwesomeApp";
import { ConfigDirectorProvider, ConfigDirectorLogger } from "@configdirector/react-native-sdk";
import NetInfo from '@react-native-community/netinfo';

export default function App() {
  return (
    <ConfigDirectorProvider
      sdkKey={"YOUR-CLIENT-SDK-KEY"}
      appName="MyAwesomeApp"
      appVersion="1.0.0"
      netInfoSubscribe={NetInfo.addEventListener}>
      <View>
        <AwesomeApp />
      </View>
    </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.

App.tsx
import { ConfigDirectorProvider } from "@configdirector/react-native-sdk";

export default function App() {
  return (
    <ConfigDirectorProvider sdkKey="YOUR-CLIENT-SDK-KEY" mode="polling" pollingInterval={120}>
      <YourApp />
    </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-native-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-native-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 { View } from "react-native";
import AwesomeApp from "./AwesomeApp";
import { ConfigDirectorProvider } from "@configdirector/react-native-sdk";

export default function App() {
  return (
    <ConfigDirectorProvider
      sdkKey={"YOUR-CLIENT-SDK-KEY"}
      appName="MyAwesomeApp"
      appVersion="1.0.0"
      context={{ id: "12345", name: "Example User", traits: { region: "North America" } }}>
      <View>
        <AwesomeApp />
      </View>
    </ConfigDirectorProvider>
  );
};

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

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

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;
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-native-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-native-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:

Home.test.tsx
import { act, render } from "@testing-library/react-native";
import { ConfigDirectorProvider } from "@configdirector/react-native-sdk";
import { createTestClient } from "@configdirector/react-native-sdk/testing";
import { Home } from "./Home";

test("shows the new checkout when the flag is on", async () => {
  const testClient = createTestClient({ values: { "new-checkout": true } });

  const view = await render(
    <ConfigDirectorProvider client={testClient.client}>
      <Home />
    </ConfigDirectorProvider>,
  );

  await view.findByText("New checkout");

  await act(() => testClient.setValue("new-checkout", false));
  view.getByText("Classic checkout");
});

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. React Native Testing Library 14 made render, fireEvent, and act asynchronous, so the examples await them; the same code works with version 13, where they return at once.

Components that render their own provider

An App component that renders <ConfigDirectorProvider sdkKey="…"> itself 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-native-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 () => {
  const view = await render(<App />);
  await view.findByText("New checkout");
});

While installed, the provider's construction props (sdkKey, url, mode, timeout, pollingInterval, logger, appName, appVersion) are ignored; context, hooks, and netInfoSubscribe apply as they do with a client prop. In an Expo Router app, renderRouter from expo-router/testing-library renders the root layout that builds the provider, so the whole app runs on the installed test 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, and so are its AppState and NetInfo subscriptions, even when it unmounts before initialize resolves.
  • 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.

Jest setup

The package and its dependency @noble/hashes are ES modules, and the package's files are .mjs files, which neither the jest-expo nor the @react-native/jest-preset preset transforms: their Babel transform only covers .js, .ts, and .tsx files. The Jest configuration needs two additions: transformIgnorePatterns that let Jest transform both packages, and a transform entry that applies the preset's Babel transform to .mjs files.

Since React Native 0.86, the React Native Jest preset is its own package, @react-native/jest-preset, and jest-expo requires it as a peer dependency. Install it next to your test runner:

npm install --save-dev @react-native/jest-preset

For an Expo app:

jest.config.js
const expoPreset = require("jest-expo/jest-preset");

module.exports = {
  preset: "jest-expo",
  transform: { "\\.mjs$": expoPreset.transform["\\.[jt]sx?$"] },
  transformIgnorePatterns: [
    "/node_modules/(?!(.pnpm|react-native|@react-native|@react-native-community|expo|@expo|@expo-google-fonts|react-navigation|@react-navigation|@sentry/react-native|native-base|standard-navigation|@configdirector|@noble))",
    "/node_modules/react-native-reanimated/plugin/",
    "/node_modules/@react-native/babel-preset/",
  ],
};

These transformIgnorePatterns are the ones jest-expo 57 sets, with @configdirector and @noble added. For another version of jest-expo, start from the transformIgnorePatterns in its jest-expo/jest-preset and add the same two packages. For an app without Expo:

jest.config.js
module.exports = {
  preset: "@react-native/jest-preset",
  transform: { "\\.mjs$": "babel-jest" },
  transformIgnorePatterns: ["node_modules/(?!((jest-)?react-native|@react-native(-community)?|@configdirector|@noble)/)"],
};

Under either preset, AppState.addEventListener is a mock that never calls its listener, so the provider never pauses on its own. A test that calls the registered listener to simulate backgrounding runs the provider's production pause and resume against the test client, where a resume always delivers the stored values at once.

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.