The availability of features may depend on your plan type. Contact your Customer Success Manager if you have any questions.
Dev guideRecipesAPI ReferenceChangelog
Dev guideAPI ReferenceRecipesChangelogUser GuideGitHubDev CommunityOptimizely AcademySubmit a ticketLog In
Dev guide

Upgrade the React Native SDK from v3 to v4

Steps to upgrade the Feature Experimentation React Native SDK from previous versions to version 4.0.0+.

This documentation is for React Native SDK versions 4.0.0 and later.

For versions 3.x and earlier, see React Native SDK prior to v4.

See the SDK compatibility matrix documentation for a list of current SDK releases and the features they support.

This guide helps you migrate your implementation from Optimizely React Native SDK v3 to v4. The new version introduces architectural changes based on JavaScript SDK v6, providing modular control over SDK components.

Major changes

v4 is a ground-up rewrite with a fundamentally different architecture:

Aspectv3v4
Underlying JS SDKv5v6
Client modelStateful ReactSDKClient wrapper (user bound to client)Thin wrapper over JS SDK Client (user managed by Provider)
Readiness model[value, clientReady, didTimeout] tuples{ decision, isLoading, error } discriminated unions
Datafile updatesautoUpdate option per hookAutomatic via SDK polling; hooks re-evaluate on config changes
User overridesPer-hook overrideUserId / overrideAttributesRemoved; use separate <OptimizelyProvider> instances
ComponentsOptimizelyExperiment, OptimizelyFeature, OptimizelyVariationRemoved; use hooks
HOCwithOptimizelyRemoved; use hooks

Breaking environment changes

v3v4
Module formatESM + CommonJSESM only (no require entry point)
Node.js>=14.0.0>=18.0.0
React peer dependency>=16.8.0>=16.8.0 (unchanged)

If your project uses CommonJS (require()), you need to switch to ESM imports or configure your bundler to handle ESM dependencies.

Note: React Native peer dependencies such as @react-native-async-storage/async-storage may still be required. See Install the React Native SDK for the full list of peer dependencies.

Client initialization

v3 (Before)

import { createInstance } from '@optimizely/react-sdk';

const optimizely = createInstance({
  sdkKey: '<YOUR_SDK_KEY>',
  datafile: window.optimizelyDatafile,
  eventBatchSize: 10,
  eventFlushInterval: 2000,
});

v4 (After)

import {
  createInstance,
  createPollingProjectConfigManager,
  createBatchEventProcessor,
} from '@optimizely/react-sdk';

const optimizely = createInstance({
  projectConfigManager: createPollingProjectConfigManager({
    sdkKey: '<YOUR_SDK_KEY>',
    datafile: window.optimizelyDatafile,
    autoUpdate: true,
  }),
  eventProcessor: createBatchEventProcessor({
    batchSize: 10,
    flushInterval: 2000,
  }),
});

Important: You must use createInstance from @optimizely/react-sdk, not from @optimizely/optimizely-sdk.

In v3, createInstance returned null on invalid config. In v4, it throws an error.

Project Configuration Management

In v4, datafile management must be configured by passing in a projectConfigManager:

Polling Project Config Manager

For automatic datafile updates:

const projectConfigManager = createPollingProjectConfigManager({
  sdkKey: '<YOUR_SDK_KEY>',
  datafile: datafileString, // optional
  autoUpdate: true,
  updateInterval: 60000, // 1 minute
});

Static Project Config Manager

For a fixed datafile with no polling:

const projectConfigManager = createStaticProjectConfigManager({
  datafile: datafileString,
});

Event Processing

In v3, a batch event processor was enabled by default. In v4, event processing is opt-in — you must pass an eventProcessor to createInstance, otherwise no events are dispatched.

Batch Event Processor

const eventProcessor = createBatchEventProcessor({
  batchSize: 10, // optional, default is 10
  flushInterval: 1000, // optional
});

Forwarding Event Processor

Sends events immediately:

const eventProcessor = createForwardingEventProcessor();

ODP Management

In v3, ODP was configured via odpOptions and enabled by default. In v4, ODP is opt-in:

v3 (Before)

const optimizely = createInstance({
  sdkKey: '<YOUR_SDK_KEY>',
  odpOptions: {
    disabled: false,
    segmentsCacheSize: 100,
    segmentsCacheTimeout: 600000,
  },
});

v4 (After)

const odpManager = createOdpManager({
  segmentsCacheSize: 100,
  segmentsCacheTimeout: 600000,
});

const optimizely = createInstance({
  projectConfigManager,
  odpManager,
});

To disable ODP in v4, simply do not pass an odpManager.

Log output

Logging is disabled by default in v4. You must pass a logger to createInstance to enable it.

v3 (Before)

import { createInstance, setLogLevel } from '@optimizely/react-sdk';

const optimizely = createInstance({
  sdkKey: '<YOUR_SDK_KEY>',
  logLevel: 'debug',
});

v4 (After)

import {
  createInstance,
  createPollingProjectConfigManager,
  createLogger,
  DEBUG,
} from '@optimizely/react-sdk';

const optimizely = createInstance({
  projectConfigManager: createPollingProjectConfigManager({
    sdkKey: '<YOUR_SDK_KEY>',
  }),
  logger: createLogger({ logLevel: DEBUG }),
});

Error Handling

v3 (Before)

const optimizely = createInstance({
  errorHandler: {
    handleError: (error) => console.error('Custom error handler', error),
  },
});

v4 (After)

const errorNotifier = createErrorNotifier({
  handleError: (error) => console.error('Custom error handler', error),
});

const optimizely = createInstance({
  projectConfigManager,
  errorNotifier,
});

Provider changes

v3 (Before)

<OptimizelyProvider
  optimizely={optimizely}
  user={{ id: 'user-123', attributes: { plan: 'gold' } }}
  timeout={500}
>
  <App />
</OptimizelyProvider>

v4 (After)

<OptimizelyProvider
  client={optimizely}
  user={{ id: 'user-123', attributes: { plan: 'gold' } }}
  timeout={500}
>
  <App />
</OptimizelyProvider>

Prop changes

v3 Propv4 PropNotes
optimizelyclientRenamed.
useruserSame shape, now also accepts null. No longer accepts a Promise. Pass null/undefined/omit for loading state; pass {} for VUID-only mode.
timeouttimeoutDefault changed from 5000 ms to 30000 ms.
userId(removed)Deprecated in v3, removed in v4. Use user.
userAttributes(removed)Deprecated in v3, removed in v4. Use user.
(new)skipSegmentsSkips ODP segment fetching. Default false.

Hooks migration

useDecision → useDecide

v3

const [decision, clientReady, didTimeout] = useDecision(
  'flag-key',
  { autoUpdate: true, timeout: 500, decideOptions: [OptimizelyDecideOption.INCLUDE_REASONS] },
  { overrideUserId: 'other-user', overrideAttributes: { plan: 'gold' } }
);

if (!clientReady) return <Loading />;
if (decision.enabled) return <NewFeature />;

v4

const { decision, isLoading, error } = useDecide('flag-key', {
  decideOptions: [OptimizelyDecideOption.INCLUDE_REASONS],
});

if (isLoading) return <Loading />;
if (error) return <ErrorDisplay error={error} />;
if (decision.enabled) return <NewFeature />;
Aspectv3 useDecisionv4 useDecide
Return type[decision, clientReady, didTimeout] tuple{ decision, isLoading, error } object
autoUpdate optionPer-hook opt-inRemoved; updates are automatic
timeout optionPer-hook overrideRemoved; set on Provider only
overrideUserIdThird argumentRemoved
overrideAttributesThird argumentRemoved
Loading state!clientReadyisLoading: true

useTrackEvent (removed)

Use useOptimizelyUserContext instead:

// v3
const [track] = useTrackEvent();
track('purchase', undefined, undefined, { revenue: 4200 });

// v4
const { userContext } = useOptimizelyUserContext();
userContext?.trackEvent('purchase', { revenue: 4200 });

useExperiment and useFeature (removed)

These hooks are removed with no hook replacement. For programmatic access, client.activate() and client.isFeatureEnabled() are still available on the client via useOptimizelyClient.

New hooks in v4

HookDescription
useDecide(flagKey, config?)Single flag decision (replaces useDecision)
useDecideForKeys(flagKeys[], config?)Batch decisions for multiple flag keys
useDecideAll(config?)Decisions for all active flags
useDecideAsync(flagKey, config?)Async variant of useDecide
useDecideForKeysAsync(flagKeys[], config?)Async variant of useDecideForKeys
useDecideAllAsync(config?)Async variant of useDecideAll
useOptimizelyClient()Returns the Optimizely Client instance
useOptimizelyUserContext()Returns { userContext, isLoading, error }

withOptimizely (removed)

The withOptimizely HOC is removed. Use the useOptimizelyClient hook instead:

// v3
import { withOptimizely } from '@optimizely/react-sdk';

class MyComponent extends React.Component {
  render() {
    const { optimizely } = this.props;
    return <div>{optimizely.decide('flag').enabled ? 'On' : 'Off'}</div>;
  }
}
export default withOptimizely(MyComponent);

// v4
import { useOptimizelyClient } from '@optimizely/react-sdk';

function MyComponent() {
  const client = useOptimizelyClient();
  return <div>...</div>;
}

Removed components

The following React components are removed in v4. Use hooks instead:

  • OptimizelyExperiment
  • OptimizelyFeature
  • OptimizelyVariation

Removed exports

  • logOnlyEventDispatcher — To disable event dispatching, do not pass an eventProcessor to createInstance.
  • setLogger / setLogLevel / logging — Use createLogger() factory.
  • errorHandler — Use createErrorNotifier().
  • enums — Removed.
  • OptimizelyContext / OptimizelyContextConsumer — Use hooks.
  • ReactSDKClient type — Renamed to Client.

onReady Promise behavior

In v3, onReady() always fulfilled with { success, reason }. In v4, onReady() fulfills when the client is ready and rejects on failure:

// v3
optimizely.onReady().then(({ success, reason }) => {
  if (success) { /* ready */ }
});

// v4
optimizely.onReady()
  .then(() => { /* ready */ })
  .catch((err) => { console.error(err); });

Note: When using hooks (useDecide, etc.), you don't call onReady directly — the Provider and hooks handle readiness internally.

TypeScript changes

Renamed types

v3v4
ReactSDKClientClient

New types

TypeDescription
UseDecideConfigConfig object for useDecide
UseDecideResultReturn type of useDecide — discriminated union
UseDecideMultiResultReturn type of useDecideForKeys / useDecideAll
OptimizelyProviderPropsProps for <OptimizelyProvider>
UserInfo{ id?: string; attributes?: UserAttributes }

Return type changes

// v3
type UseDecisionReturn = [OptimizelyDecision, boolean, boolean];

// v4
type UseDecideResult =
  | { isLoading: true;  error: null;  decision: null }
  | { isLoading: false; error: Error; decision: null }
  | { isLoading: false; error: null;  decision: OptimizelyDecision };

Migration examples

Basic example

v3 (Before)

import { createInstance, OptimizelyProvider, useDecision } from '@optimizely/react-sdk';

const optimizely = createInstance({ sdkKey: '<YOUR_SDK_KEY>' });

function Feature() {
  const [decision] = useDecision('flag-key');
  return decision.enabled ? <NewFeature /> : <Default />;
}

<OptimizelyProvider optimizely={optimizely} user={{ id: 'user-123' }}>
  <Feature />
</OptimizelyProvider>

v4 (After)

import {
  createInstance,
  createPollingProjectConfigManager,
  createBatchEventProcessor,
  OptimizelyProvider,
  useDecide,
} from '@optimizely/react-sdk';

const optimizely = createInstance({
  projectConfigManager: createPollingProjectConfigManager({
    sdkKey: '<YOUR_SDK_KEY>',
  }),
  eventProcessor: createBatchEventProcessor(),
});

function Feature() {
  const { decision, isLoading, error } = useDecide('flag-key');
  if (isLoading) return <Loading />;
  if (error) return <ErrorDisplay error={error} />;
  return decision.enabled ? <NewFeature /> : <Default />;
}

<OptimizelyProvider client={optimizely} user={{ id: 'user-123' }}>
  <Feature />
</OptimizelyProvider>

Complete example with event tracking and ODP

v3 (Before)

import { createInstance, OptimizelyProvider, useDecision, useTrackEvent } from '@optimizely/react-sdk';

const optimizely = createInstance({
  sdkKey: '<YOUR_SDK_KEY>',
  eventBatchSize: 3,
  eventFlushInterval: 10000,
  odpOptions: { segmentsCacheSize: 10 },
});

function Feature() {
  const [decision] = useDecision('flag-key', { autoUpdate: true });
  const [track] = useTrackEvent();

  return (
    <div>
      {decision.enabled && <NewFeature />}
      <button onClick={() => track('purchase')}>Buy</button>
    </div>
  );
}

v4 (After)

import {
  createInstance,
  createPollingProjectConfigManager,
  createBatchEventProcessor,
  createOdpManager,
  createLogger,
  DEBUG,
  OptimizelyProvider,
  useDecide,
  useOptimizelyUserContext,
} from '@optimizely/react-sdk';

const optimizely = createInstance({
  projectConfigManager: createPollingProjectConfigManager({
    sdkKey: '<YOUR_SDK_KEY>',
  }),
  eventProcessor: createBatchEventProcessor({
    batchSize: 3,
    flushInterval: 10000,
  }),
  odpManager: createOdpManager({ segmentsCacheSize: 10 }),
  logger: createLogger({ logLevel: DEBUG }),
});

function Feature() {
  const { decision, isLoading, error } = useDecide('flag-key');
  const { userContext } = useOptimizelyUserContext();

  if (isLoading) return <Loading />;
  if (error) return <ErrorDisplay error={error} />;

  return (
    <div>
      {decision.enabled && <NewFeature />}
      <button onClick={() => userContext?.trackEvent('purchase')}>Buy</button>
    </div>
  );
}

For complete implementation examples, refer to the React SDK GitHub repository.