React Native SDK
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
yarn add @configdirector/react-native-sdk
pnpm add @configdirector/react-native-sdk
bun add @configdirector/react-native-sdk
Configure and initialize the provider
Initialize the provider with your client SDK key:
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.
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:
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:
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:
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.
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.
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:
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:
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:
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:
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;
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:
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;
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:
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:
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
clientprop orinstallTestClient. It disposes only a client it built fromsdkKey. - It does not call
initializeon a client that is ready or initializing. If the client is ready and thecontextprop is given and differs from the client's context, it callsupdateContext. A provider given a ready client reports thereadystatus on its first render. hooksgiven with aclientare registered on it while the provider is mounted and removed when it unmounts, like every other handler the provider registers, and so are itsAppStateand NetInfo subscriptions, even when it unmounts beforeinitializeresolves.- The
contextprop is compared by deep equality, on mount and on later renders, so a new object with the same contents does not reconnect. - A
clientprop that changes after the provider mounted is ignored with a warning. Remount the provider, for example with a newkey, 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
yarn add --dev @react-native/jest-preset
pnpm add --save-dev @react-native/jest-preset
bun add --dev @react-native/jest-preset
For an Expo app:
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:
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.