Skip to content

Commit 45f3b78

Browse files
committed
Merge branch 'main' into chore/remove-deprecated-tslint-dependency
2 parents 6b1fbf6 + a8b52f4 commit 45f3b78

12 files changed

+257
-61
lines changed

src/asyncWithLDProvider.test.tsx

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import React from 'react';
22
import '@testing-library/dom';
33
import '@testing-library/jest-dom';
44
import { render } from '@testing-library/react';
5-
import { initialize, LDContext, LDFlagChangeset, LDOptions } from 'launchdarkly-js-client-sdk';
5+
import { initialize, LDClient, LDContext, LDFlagChangeset, LDOptions } from 'launchdarkly-js-client-sdk';
66
import { AsyncProviderConfig, LDReactOptions } from './types';
7-
import { Consumer } from './context';
7+
import { Consumer, reactSdkContextFactory } from './context';
88
import asyncWithLDProvider from './asyncWithLDProvider';
99
import wrapperOptions from './wrapperOptions';
1010
import { fetchFlags } from './utils';
1111

12-
1312
jest.mock('launchdarkly-js-client-sdk', () => {
1413
const actual = jest.requireActual('launchdarkly-js-client-sdk');
1514

@@ -351,4 +350,120 @@ describe('asyncWithLDProvider', () => {
351350

352351
expect(receivedNode).toHaveTextContent('{"testFlag":false}');
353352
});
353+
354+
test('custom context is provided to consumer', async () => {
355+
const CustomContext = reactSdkContextFactory();
356+
const customLDClient = {
357+
on: jest.fn((_: string, cb: () => void) => {
358+
cb();
359+
}),
360+
off: jest.fn(),
361+
allFlags: jest.fn().mockReturnValue({ 'context-test-flag': true }),
362+
variation: jest.fn((_: string, v) => v),
363+
waitForInitialization: jest.fn(),
364+
};
365+
const config: AsyncProviderConfig = {
366+
clientSideID,
367+
ldClient: customLDClient as unknown as LDClient,
368+
reactOptions: {
369+
reactContext: CustomContext,
370+
},
371+
};
372+
const originalUtilsModule = jest.requireActual('./utils');
373+
mockFetchFlags.mockImplementation(originalUtilsModule.fetchFlags);
374+
375+
const LDProvider = await asyncWithLDProvider(config);
376+
const LaunchDarklyApp = (
377+
<LDProvider>
378+
<CustomContext.Consumer>
379+
{({ flags }) => {
380+
return (
381+
<span>
382+
flag is {flags.contextTestFlag === undefined ? 'undefined' : JSON.stringify(flags.contextTestFlag)}
383+
</span>
384+
);
385+
}}
386+
</CustomContext.Consumer>
387+
</LDProvider>
388+
);
389+
390+
const { findByText } = render(LaunchDarklyApp);
391+
expect(await findByText('flag is true')).not.toBeNull();
392+
393+
const receivedNode = await renderWithConfig({ clientSideID });
394+
expect(receivedNode).not.toHaveTextContent('{"contextTestFlag":true}');
395+
});
396+
397+
test('multiple providers', async () => {
398+
const customLDClient1 = {
399+
on: jest.fn((_: string, cb: () => void) => {
400+
cb();
401+
}),
402+
off: jest.fn(),
403+
allFlags: jest.fn().mockReturnValue({ 'context1-test-flag': true }),
404+
variation: jest.fn((_: string, v) => v),
405+
waitForInitialization: jest.fn(),
406+
};
407+
const customLDClient2 = {
408+
on: jest.fn((_: string, cb: () => void) => {
409+
cb();
410+
}),
411+
off: jest.fn(),
412+
allFlags: jest.fn().mockReturnValue({ 'context2-test-flag': true }),
413+
variation: jest.fn((_: string, v) => v),
414+
waitForInitialization: jest.fn(),
415+
};
416+
const originalUtilsModule = jest.requireActual('./utils');
417+
mockFetchFlags.mockImplementation(originalUtilsModule.fetchFlags);
418+
419+
const CustomContext1 = reactSdkContextFactory();
420+
const LDProvider1 = await asyncWithLDProvider({
421+
clientSideID,
422+
ldClient: customLDClient1 as unknown as LDClient,
423+
reactOptions: {
424+
reactContext: CustomContext1,
425+
},
426+
});
427+
const CustomContext2 = reactSdkContextFactory();
428+
const LDProvider2 = await asyncWithLDProvider({
429+
clientSideID,
430+
ldClient: customLDClient2 as unknown as LDClient,
431+
reactOptions: {
432+
reactContext: CustomContext2,
433+
},
434+
});
435+
const safeValue = (val?: boolean) => (val === undefined ? 'undefined' : JSON.stringify(val));
436+
const LaunchDarklyApp = (
437+
<LDProvider1>
438+
<LDProvider2>
439+
<CustomContext1.Consumer>
440+
{({ flags }) => {
441+
return (
442+
<>
443+
<span>consumer 1, flag 1 is {safeValue(flags.context1TestFlag)}</span>
444+
<span>consumer 1, flag 2 is {safeValue(flags.context2TestFlag)}</span>
445+
</>
446+
);
447+
}}
448+
</CustomContext1.Consumer>
449+
<CustomContext2.Consumer>
450+
{({ flags }) => {
451+
return (
452+
<>
453+
<span>consumer 2, flag 1 is {safeValue(flags.context1TestFlag)}</span>
454+
<span>consumer 2, flag 2 is {safeValue(flags.context2TestFlag)}</span>
455+
</>
456+
);
457+
}}
458+
</CustomContext2.Consumer>
459+
</LDProvider2>
460+
</LDProvider1>
461+
);
462+
463+
const { findByText } = render(LaunchDarklyApp);
464+
expect(await findByText('consumer 1, flag 1 is true')).not.toBeNull();
465+
expect(await findByText('consumer 1, flag 2 is undefined')).not.toBeNull();
466+
expect(await findByText('consumer 2, flag 1 is undefined')).not.toBeNull();
467+
expect(await findByText('consumer 2, flag 2 is true')).not.toBeNull();
468+
});
354469
});

src/asyncWithLDProvider.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { useState, useEffect, ReactNode } from 'react';
22
import { initialize, LDFlagChangeset } from 'launchdarkly-js-client-sdk';
33
import { AsyncProviderConfig, defaultReactOptions } from './types';
4-
import { Provider } from './context';
54
import { fetchFlags, getContextOrUser, getFlattenedFlagsFromChangeset } from './utils';
65
import getFlagsProxy from './getFlagsProxy';
76
import wrapperOptions from './wrapperOptions';
@@ -104,7 +103,9 @@ export default async function asyncWithLDProvider(config: AsyncProviderConfig) {
104103
// unproxiedFlags is for internal use only. Exclude it from context.
105104
const { unproxiedFlags: _, ...rest } = ldData;
106105

107-
return <Provider value={rest}>{children}</Provider>;
106+
const { reactContext } = reactOptions;
107+
108+
return <reactContext.Provider value={rest}>{children}</reactContext.Provider>;
108109
};
109110

110111
return LDProvider;

src/context.ts

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,17 @@
11
import { createContext } from 'react';
2-
import { LDClient, LDFlagSet } from 'launchdarkly-js-client-sdk';
3-
import { LDFlagKeyMap } from './types';
2+
import { ReactSdkContext } from './types';
43

54
/**
6-
* The sdk context stored in the Provider state and passed to consumers.
5+
* `reactSdkContextFactory` is a function useful for creating a React context for use with
6+
* all the providers and consumers in this library.
7+
*
8+
* @return a React Context
79
*/
8-
interface ReactSdkContext {
9-
/**
10-
* JavaScript proxy that will trigger a LDClient#variation call on flag read in order
11-
* to register a flag evaluation event in LaunchDarkly. Empty {} initially
12-
* until flags are fetched from the LaunchDarkly servers.
13-
*/
14-
flags: LDFlagSet;
15-
16-
/**
17-
* Map of camelized flag keys to their original unmodified form. Empty if useCamelCaseFlagKeys option is false.
18-
*/
19-
flagKeyMap: LDFlagKeyMap;
20-
21-
/**
22-
* An instance of `LDClient` from the LaunchDarkly JS SDK (`launchdarkly-js-client-sdk`).
23-
* This will be be undefined initially until initialization is complete.
24-
*
25-
* @see https://docs.launchdarkly.com/sdk/client-side/javascript
26-
*/
27-
ldClient?: LDClient;
28-
29-
/**
30-
* LaunchDarkly client initialization error, if there was one.
31-
*/
32-
error?: Error;
33-
}
34-
10+
const reactSdkContextFactory = () => createContext<ReactSdkContext>({ flags: {}, flagKeyMap: {}, ldClient: undefined });
3511
/**
3612
* @ignore
3713
*/
38-
const context = createContext<ReactSdkContext>({ flags: {}, flagKeyMap: {}, ldClient: undefined });
14+
const context = reactSdkContextFactory();
3915
const {
4016
/**
4117
* @ignore
@@ -47,5 +23,5 @@ const {
4723
Consumer,
4824
} = context;
4925

50-
export { Provider, Consumer, ReactSdkContext };
26+
export { Provider, Consumer, ReactSdkContext, reactSdkContextFactory };
5127
export default context;

src/getFlagsProxy.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const camelizedFlags: LDFlagSet = {
1313
};
1414

1515
// cast as unknown first to be able to partially mock ldClient
16-
const ldClient = ({ variation: jest.fn((flagKey) => rawFlags[flagKey] as string) } as unknown) as LDClient;
16+
const ldClient = { variation: jest.fn((flagKey) => rawFlags[flagKey] as string) } as unknown as LDClient;
1717

1818
beforeEach(jest.clearAllMocks);
1919

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import useFlags from './useFlags';
66
import useLDClient from './useLDClient';
77
import useLDClientError from './useLDClientError';
88
import { camelCaseKeys } from './utils';
9+
import { reactSdkContextFactory } from './context';
910

1011
export * from './types';
1112

1213
export {
1314
LDProvider,
1415
asyncWithLDProvider,
1516
camelCaseKeys,
17+
reactSdkContextFactory,
1618
useFlags,
1719
useLDClient,
1820
useLDClientError,

src/provider.test.tsx

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,21 @@ jest.mock('./utils', () => {
1616
fetchFlags: jest.fn(),
1717
};
1818
});
19-
jest.mock('./context', () => ({ Provider: 'Provider' }));
19+
jest.mock('./context', () => {
20+
const originalModule = jest.requireActual('./context');
21+
22+
return {
23+
...originalModule,
24+
Provider: 'Provider',
25+
};
26+
});
2027

2128
import React, { Component } from 'react';
29+
import { render } from '@testing-library/react';
2230
import { create } from 'react-test-renderer';
2331
import { initialize, LDClient, LDContext, LDFlagChangeset, LDOptions } from 'launchdarkly-js-client-sdk';
2432
import { LDReactOptions, EnhancedComponent, ProviderConfig } from './types';
25-
import { ReactSdkContext as HocState } from './context';
33+
import { ReactSdkContext as HocState, reactSdkContextFactory } from './context';
2634
import LDProvider from './provider';
2735
import { fetchFlags } from './utils';
2836
import wrapperOptions from './wrapperOptions';
@@ -126,7 +134,7 @@ describe('LDProvider', () => {
126134

127135
test('ld client is used if passed in', async () => {
128136
options = { ...options, bootstrap: {} };
129-
const ldClient = (mockLDClient as unknown) as LDClient;
137+
const ldClient = mockLDClient as unknown as LDClient;
130138
mockInitialize.mockClear();
131139
const props: ProviderConfig = { clientSideID, ldClient };
132140
const LaunchDarklyApp = (
@@ -144,7 +152,7 @@ describe('LDProvider', () => {
144152
const context2: LDContext = { key: 'launch', kind: 'user', name: 'darkly' };
145153
options = { ...options, bootstrap: {} };
146154
const ldClient = new Promise<LDClient>((resolve) => {
147-
resolve((mockLDClient as unknown) as LDClient);
155+
resolve(mockLDClient as unknown as LDClient);
148156

149157
return;
150158
});
@@ -537,4 +545,43 @@ describe('LDProvider', () => {
537545
flagKeyMap: { testFlag: 'test-flag' },
538546
});
539547
});
548+
549+
test('custom context is provided to consumer', async () => {
550+
const CustomContext = reactSdkContextFactory();
551+
const customLDClient = {
552+
on: jest.fn((_: string, cb: () => void) => {
553+
cb();
554+
}),
555+
off: jest.fn(),
556+
allFlags: jest.fn().mockReturnValue({ 'context-test-flag': true }),
557+
variation: jest.fn((_: string, v) => v),
558+
waitForInitialization: jest.fn(),
559+
};
560+
const props: ProviderConfig = {
561+
clientSideID,
562+
ldClient: customLDClient as unknown as LDClient,
563+
reactOptions: {
564+
reactContext: CustomContext,
565+
},
566+
};
567+
const originalUtilsModule = jest.requireActual('./utils');
568+
mockFetchFlags.mockImplementation(originalUtilsModule.fetchFlags);
569+
570+
const LaunchDarklyApp = (
571+
<LDProvider {...props}>
572+
<CustomContext.Consumer>
573+
{({ flags }) => {
574+
return (
575+
<span>
576+
flag is {flags.contextTestFlag === undefined ? 'undefined' : JSON.stringify(flags.contextTestFlag)}
577+
</span>
578+
);
579+
}}
580+
</CustomContext.Consumer>
581+
</LDProvider>
582+
);
583+
584+
const { findByText } = render(LaunchDarklyApp);
585+
expect(await findByText('flag is true')).not.toBeNull();
586+
});
540587
});

src/provider.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { Component, PropsWithChildren } from 'react';
22
import { initialize, LDClient, LDFlagChangeset, LDFlagSet } from 'launchdarkly-js-client-sdk';
33
import { EnhancedComponent, ProviderConfig, defaultReactOptions, LDReactOptions } from './types';
4-
import { Provider } from './context';
54
import { camelCaseKeys, fetchFlags, getContextOrUser, getFlattenedFlagsFromChangeset } from './utils';
65
import getFlagsProxy from './getFlagsProxy';
76
import wrapperOptions from './wrapperOptions';
@@ -144,7 +143,13 @@ class LDProvider extends Component<PropsWithChildren<ProviderConfig>, ProviderSt
144143
render() {
145144
const { flags, flagKeyMap, ldClient, error } = this.state;
146145

147-
return <Provider value={{ flags, flagKeyMap, ldClient, error }}>{this.props.children}</Provider>;
146+
const { reactContext } = this.getReactOptions();
147+
148+
return (
149+
<reactContext.Provider value={{ flags, flagKeyMap, ldClient, error }}>
150+
{this.props.children}
151+
</reactContext.Provider>
152+
);
148153
}
149154
}
150155

0 commit comments

Comments
 (0)