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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ jest.mock('../../../src/client/LDReactClient', () => ({
}));

// ─── createLDReactProviderWithClient ────────────────────────────────────────

it('renders children', () => {
const client = makeMockClient();
const Provider = createLDReactProviderWithClient(client);
Expand Down Expand Up @@ -219,6 +218,31 @@ describe('createLDReactProvider (convenience factory)', () => {
expect(mockClient.start).toHaveBeenCalledWith(startOptions);
});

it('merges bootstrap into startOptions when bootstrap is provided', () => {
const bootstrapData = { $flagsState: { flag: { variation: 0 } }, $valid: true };
createLDReactProvider('sdk-key', { kind: 'user', key: 'u1' }, { bootstrap: bootstrapData });

expect(mockClient.start).toHaveBeenCalledWith({ bootstrap: bootstrapData });
});

it('merges bootstrap with existing startOptions', () => {
const bootstrapData = { $flagsState: { flag: { variation: 0 } }, $valid: true };
const startOptions = { timeout: 5 };
createLDReactProvider(
'sdk-key',
{ kind: 'user', key: 'u1' },
{ startOptions, bootstrap: bootstrapData },
);

expect(mockClient.start).toHaveBeenCalledWith({ timeout: 5, bootstrap: bootstrapData });
});

it('does not merge bootstrap into startOptions when bootstrap is not provided', () => {
createLDReactProvider('sdk-key', { kind: 'user', key: 'u1' });

expect(mockClient.start).toHaveBeenCalledWith(undefined);
});

it('uses provided reactContext option', () => {
const CustomContext = initLDReactContext();
const contextValues: LDReactClientContextValue[] = [];
Expand Down
13 changes: 13 additions & 0 deletions packages/sdk/react/src/client/LDOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,17 @@ export interface LDReactProviderOptions {
* @returns {LDReactClientContext} The react context for the LaunchDarkly client.
*/
reactContext?: LDReactClientContext;

/**
* Bootstrap data from the server. Pass the result of `flagsState.toJSON()` obtained
* from the server SDK's `allFlagsState` method.
*
* When provided, the client immediately uses these values before the first network
* response arrives — eliminating the flag-fetch waterfall on page load.
*
* This is merged into `startOptions.bootstrap` when the client is started. If both
* `bootstrap` and `startOptions.bootstrap` are provided, this top-level value takes
* precedence.
*/
bootstrap?: unknown;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
5 changes: 3 additions & 2 deletions packages/sdk/react/src/client/provider/LDReactProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,13 @@ export function createLDReactProvider(
context: LDContext,
options?: LDReactProviderOptions,
): React.FC<{ children: React.ReactNode }> {
const { deferInitialization, startOptions, reactContext, ldOptions } = options ?? {};
const { deferInitialization, startOptions, reactContext, ldOptions, bootstrap } = options ?? {};

const client = createClient(clientSideID, context, ldOptions);

if (!deferInitialization) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured if users are deferring start then they will need to provide their own start options. We may need to call this out in docs somewhere.

client.start(startOptions);
const effectiveStartOptions = bootstrap ? { ...startOptions, bootstrap } : startOptions;
client.start(effectiveStartOptions);
}
Comment on lines 127 to 130
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Bootstrap data is silently discarded when deferInitialization: true

When a user passes { bootstrap: myData, deferInitialization: true } to createLDReactProvider, the bootstrap value is destructured from options at line 119 but never used because the if (!deferInitialization) block (lines 123-126) is skipped entirely. Since createLDReactProvider returns a React.FC (not the client), the user has no way to recover the lost bootstrap data. The only way to later call start() is through the useLDClient() hook (packages/sdk/react/src/client/hooks/useLDClient.ts:14), but the bootstrap data from the original options is inaccessible at that point. The API happily accepts this combination without any warning, silently discarding the user's bootstrap data.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine as we should have it documented that if developers decide to defer initialization then they are responsible for starting the client however they want.


return createLDReactProviderWithClient(client, reactContext);
Expand Down
30 changes: 30 additions & 0 deletions packages/sdk/react/temp_docs/MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,36 @@ state when initialization completes or when `client.identify()` is called.

---

## Bootstrap

### Old SDK

Bootstrap data was passed nested inside `options`:

```tsx
// HOC
withLDProvider({ clientSideID: 'your-id', options: { bootstrap: myData } })(App);

// Async HOC
const LDProvider = await asyncWithLDProvider({ clientSideID: 'your-id', options: { bootstrap: myData } });
```

### New SDK

Bootstrap is a first-class option on `createLDReactProvider`:

```tsx
import { createLDReactProvider } from '@launchdarkly/react-sdk';

const LDProvider = createLDReactProvider('your-client-side-id', { kind: 'user', key: 'user-key' }, {
bootstrap: myData,
});
```

The `bootstrap` data format is unchanged from the old SDK. You can pass either a plain key-value
object (`{ 'my-flag': true }`) or the output of `allFlagsState().toJSON()`, which includes
`$flagsState` and `$valid` metadata.

## Removed APIs

| Old API | Status | Replacement |
Expand Down
Loading