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:
| Aspect | v3 | v4 |
|---|---|---|
| Underlying JS SDK | v5 | v6 |
| Client model | Stateful 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 updates | autoUpdate option per hook | Automatic via SDK polling; hooks re-evaluate on config changes |
| User overrides | Per-hook overrideUserId / overrideAttributes | Removed; use separate <OptimizelyProvider> instances |
| Components | OptimizelyExperiment, OptimizelyFeature, OptimizelyVariation | Removed; use hooks |
| HOC | withOptimizely | Removed; use hooks |
Breaking environment changes
| v3 | v4 | |
|---|---|---|
| Module format | ESM + CommonJS | ESM 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-storagemay 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
createInstancefrom@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 Prop | v4 Prop | Notes |
|---|---|---|
optimizely | client | Renamed. |
user | user | Same shape, now also accepts null. No longer accepts a Promise. Pass null/undefined/omit for loading state; pass {} for VUID-only mode. |
timeout | timeout | Default 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) | skipSegments | Skips 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 />;| Aspect | v3 useDecision | v4 useDecide |
|---|---|---|
| Return type | [decision, clientReady, didTimeout] tuple | { decision, isLoading, error } object |
autoUpdate option | Per-hook opt-in | Removed; updates are automatic |
timeout option | Per-hook override | Removed; set on Provider only |
overrideUserId | Third argument | Removed |
overrideAttributes | Third argument | Removed |
| Loading state | !clientReady | isLoading: 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
| Hook | Description |
|---|---|
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:
OptimizelyExperimentOptimizelyFeatureOptimizelyVariation
Removed exports
logOnlyEventDispatcher— To disable event dispatching, do not pass aneventProcessortocreateInstance.setLogger/setLogLevel/logging— UsecreateLogger()factory.errorHandler— UsecreateErrorNotifier().enums— Removed.OptimizelyContext/OptimizelyContextConsumer— Use hooks.ReactSDKClienttype — Renamed toClient.
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 callonReadydirectly — the Provider and hooks handle readiness internally.
TypeScript changes
Renamed types
| v3 | v4 |
|---|---|
ReactSDKClient | Client |
New types
| Type | Description |
|---|---|
UseDecideConfig | Config object for useDecide |
UseDecideResult | Return type of useDecide — discriminated union |
UseDecideMultiResult | Return type of useDecideForKeys / useDecideAll |
OptimizelyProviderProps | Props 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.
Updated 3 days ago
