---
title: Upgrade the React Native SDK from v3 to v4
slug: feature-experimentation/docs/upgrade-the-react-native-sdk-from-v3-to-v4
docTags: 
createdAt: 2026-08-05T20:39:59.000Z
---

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](docId\:cUaUqmczF4GQ3od1DQdye).

See the [SDK compatibility matrix](docId:2shbZfPCkPAZ0mlZpL1TU) 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.

:::BlockQuote
**Note:** React Native peer dependencies such as `@react-native-async-storage/async-storage` may still be required. See [Install the React Native SDK](docId\:xQ_8Rfi1K-xtj_8PBK4Li) for the full list of peer dependencies.
:::

## Client initialization

### v3 (Before)

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

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

### v4 (After)

```javascript
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,
  }),
});
```

:::BlockQuote
**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:

```javascript
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:

```javascript
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

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

### Forwarding Event Processor

Sends events immediately:

```javascript
const eventProcessor = createForwardingEventProcessor();
```

## ODP Management

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

### v3 (Before)

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

### v4 (After)

```javascript
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)

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

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

### v4 (After)

```javascript
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)

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

### v4 (After)

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

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

## Provider changes

### v3 (Before)

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

### v4 (After)

```javascript
<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

```javascript
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

```javascript
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:

```javascript
// 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:

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

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

// v4
import { View, Text } from 'react-native';
import { useOptimizelyClient } from '@optimizely/react-sdk';

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

## 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:

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

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

:::BlockQuote
**Note:** When using hooks (`useDecide`, etc.), you don't call `onReady` directly — 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

```typescript
// 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)

```javascript
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)

```javascript
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)

```javascript
import { Text, TouchableOpacity, View } from 'react-native';
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 (
    <View>
      {decision.enabled && <NewFeature />}
      <TouchableOpacity onPress={() => track('purchase')}>
        <Text>Buy</Text>
      </TouchableOpacity>
    </View>
  );
}
```

### v4 (After)

```javascript
import { Text, TouchableOpacity, View } from 'react-native';
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 (
    <View>
      {decision.enabled && <NewFeature />}
      <TouchableOpacity onPress={() => userContext?.trackEvent('purchase')}>
        <Text>Buy</Text>
      </TouchableOpacity>
    </View>
  );
}
```

For complete implementation examples, refer to the [React SDK GitHub repository](https://github.com/optimizely/react-sdk).
