Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,44 @@ _props_
| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `client` | `Client` | Yes | Instance created from `createInstance`. |
| `user` | `{ id?: string; attributes?: UserAttributes }` | No | User info object — `id` and `attributes` will be used to create the user context for all decisions and event tracking. |
| `user` | `{ id?: string; attributes?: UserAttributes } \| null` | No | User info object — `id` and `attributes` will be used to create the user context for all decisions and event tracking. Pass `null`, `undefined`, or omit while user info is being fetched — hooks will return `{ isLoading: true }` until a resolved user is provided. For VUID-only mode (no user ID), pass `user={{}}`. |
| `timeout` | `number` | No | Maximum time (in milliseconds) to wait for the SDK to become ready before hooks resolve with a loading state. Default: `30000`. |
| `qualifiedSegments` | `string[]` | No | Pre-fetched ODP audience segments for the user. Use [`getQualifiedSegments`](#getqualifiedsegments) to obtain these segments server-side. |
| `skipSegments` | `boolean` | No | When `true`, skips background ODP segment fetching. Default: `false`. |

> **Note:** Unless VUID is enabled, `<OptimizelyProvider>` requires user data. If user information must be fetched asynchronously, resolve the promise before rendering the Provider.
> **Note:** If user information is not yet available, you can render `<OptimizelyProvider>` without it — hooks will return `{ isLoading: true }` until a resolved user is provided. For VUID-only mode (no user ID), pass `user={{}}`.

#### VUID-only example

```jsx
import {
createInstance,
createPollingProjectConfigManager,
createBatchEventProcessor,
createOdpManager,
createVuidManager,
OptimizelyProvider,
} from '@optimizely/react-sdk';

const optimizely = createInstance({
projectConfigManager: createPollingProjectConfigManager({
sdkKey: 'your-optimizely-sdk-key',
}),
eventProcessor: createBatchEventProcessor(),
odpManager: createOdpManager(),
vuidManager: createVuidManager({
enableVuid: true,
}),
});

function App() {
return (
<OptimizelyProvider client={optimizely} user={{}}>
<MyComponent />
</OptimizelyProvider>
);
}
```

### Readiness

Expand Down
5 changes: 4 additions & 1 deletion docs/nextjs-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,12 +445,15 @@ export default function MyFeature() {

### User Promise not supported

User `Promise` is not supported. You must provide a resolved user object to `OptimizelyProvider`. If user information must be fetched asynchronously, resolve the promise before rendering the Provider:
User `Promise` is not supported. You can pass `null`, `undefined`, or omit the `user` prop while user information is being fetched — hooks will return `{ isLoading: true }` until a resolved user object is provided. For VUID-only mode (no user ID), pass `user={{}}`.

```tsx
// Supported
<OptimizelyProvider client={optimizely} user={{ id: 'user123', attributes: { plan: 'premium' } }} />

// Supported — hooks return { isLoading: true } until user is provided
<OptimizelyProvider client={optimizely} user={null} />

// NOT supported
<OptimizelyProvider client={optimizely} user={fetchUserPromise} />
```
Expand Down
78 changes: 75 additions & 3 deletions src/provider/OptimizelyProvider.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ describe('OptimizelyProvider', () => {
let capturedContext: OptimizelyContextValue | null = null;

const { unmount } = render(
<OptimizelyProvider client={mockClient}>
<OptimizelyProvider client={mockClient} user={{ id: 'user-1' }}>
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
</OptimizelyProvider>
);
Expand Down Expand Up @@ -488,7 +488,7 @@ describe('OptimizelyProvider', () => {
expect(mockClient.createUserContext).toHaveBeenCalledTimes(1);
});

it('should create user context without userId when user prop is not provided', async () => {
it('should not create user context when user prop is not provided', async () => {
const mockClient = createMockClient();

render(
Expand All @@ -497,7 +497,7 @@ describe('OptimizelyProvider', () => {
</OptimizelyProvider>
);

expect(mockClient.createUserContext).toHaveBeenCalledWith(undefined, undefined);
expect(mockClient.createUserContext).not.toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -793,6 +793,78 @@ describe('OptimizelyProvider', () => {
});
});

describe('null user', () => {
it('should not create user context when user is null', async () => {
const mockClient = createMockClient();

render(
<OptimizelyProvider client={mockClient} user={null}>
<div>Child</div>
</OptimizelyProvider>
);

expect(mockClient.createUserContext).not.toHaveBeenCalled();
});

it('should have null userContext in store when user is null', async () => {
const mockClient = createMockClient();
let capturedContext: OptimizelyContextValue | null = null;

render(
<OptimizelyProvider client={mockClient} user={null}>
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
</OptimizelyProvider>
);

expect(capturedContext).not.toBeNull();
expect(capturedContext!.store.getState().userContext).toBeNull();
});

it('should create context when user changes from null to valid', async () => {
const mockClient = createMockClient();
let capturedContext: OptimizelyContextValue | null = null;

const { rerender } = render(
<OptimizelyProvider client={mockClient} user={null}>
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
</OptimizelyProvider>
);

expect(mockClient.createUserContext).not.toHaveBeenCalled();
expect(capturedContext!.store.getState().userContext).toBeNull();

rerender(
<OptimizelyProvider client={mockClient} user={{ id: 'user-1' }}>
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
</OptimizelyProvider>
);

expect(mockClient.createUserContext).toHaveBeenCalledWith('user-1', undefined);
Comment thread
junaed-optimizely marked this conversation as resolved.
expect(capturedContext!.store.getState().userContext).not.toBeNull();
});

it('should set store userContext to null when user changes from valid to null', async () => {
const mockClient = createMockClient();
let capturedContext: OptimizelyContextValue | null = null;

const { rerender } = render(
<OptimizelyProvider client={mockClient} user={{ id: 'user-1' }}>
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
</OptimizelyProvider>
);

expect(capturedContext!.store.getState().userContext).not.toBeNull();

rerender(
<OptimizelyProvider client={mockClient} user={null}>
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
</OptimizelyProvider>
);

expect(capturedContext!.store.getState().userContext).toBeNull();
});
});

Comment thread
junaed-optimizely marked this conversation as resolved.
describe('context reference identity', () => {
it('should change context value reference when client changes', async () => {
const mockClient1 = createMockClient();
Expand Down
2 changes: 1 addition & 1 deletion src/provider/OptimizelyProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function OptimizelyProvider({

userManagerRef.current = new UserContextManager({
client,
onUserContextReady: (ctx) => store.setUserContext(ctx),
onUserContextChange: (ctx) => store.setUserContext(ctx),
onError: (error) => store.setError(error),
});

Expand Down
2 changes: 1 addition & 1 deletion src/provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface OptimizelyProviderProps {
/**
* User information for decisions.
*/
user?: UserInfo;
user?: UserInfo | null;

/**
* Timeout in milliseconds to wait for the client to become ready.
Expand Down
Loading
Loading