Skip to content

Commit fb17d21

Browse files
prepare 2.27.0 release (#150)
Added: - useFlags hook is now generically typed, allowing you to assert what type your flag set will be. - useLDClientError hook for exposing client initialization failures. Changed: - sendEventsOnlyForVariation is now set to true by default to prevent a flag evaluation event being generated for every flag on load. - flags object (that is either injected via props using LDConsumer or returned from the useFlags hook) will generate a - flag evaluation event on flag read (using a JavaScript proxy). This can be disabled by setting reactOptions.sendEventsOnFlagRead: false. - upgraded from ES5 to ES6.
1 parent c68676a commit fb17d21

26 files changed

+475
-224
lines changed

.github/pull_request_template.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
**Requirements**
22

33
- [ ] I have added test coverage for new or changed functionality
4-
- [ ] I have followed the repository's [pull request submission guidelines](../blob/master/CONTRIBUTING.md#submitting-pull-requests)
4+
- [ ] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests)
55
- [ ] I have validated my changes against all supported platform versions
66

77
**Related issues**

.ldrelease/config.yml

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
version: 2
2+
13
repo:
24
public: react-client-sdk
35
private: react-client-sdk-private
@@ -6,11 +8,14 @@ publications:
68
- url: https://www.npmjs.com/package/launchdarkly-react-client-sdk
79
description: npm
810

9-
template:
10-
name: npm
11+
jobs:
12+
- docker:
13+
image: node:16-buster
14+
template:
15+
name: npm
1116

1217
documentation:
13-
githubPages: true
18+
gitHubPages: true
1419
title: LaunchDarkly React SDK
1520

1621
sdk:

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Change log
22

3-
All notable changes to the LaunchDarkly Client-side SDK for React will be documented in this file. For the source code for versions 2.13.0 and earlier, see the corresponding tags in the [js-client-sdk](https://github.com/launchdarkly/js-client-sdk) repository; this code was previously in a monorepo package there. See also the [JavaScript SDK changelog](https://github.com/launchdarkly/js-client-sdk/blob/master/CHANGELOG.md), since the React SDK inherits all of the underlying functionality of the JavaScript SDK; this file covers only changes that are specific to the React interface. This project adheres to [Semantic Versioning](http://semver.org).
3+
All notable changes to the LaunchDarkly Client-side SDK for React will be documented in this file. For the source code for versions 2.13.0 and earlier, see the corresponding tags in the [js-client-sdk](https://github.com/launchdarkly/js-client-sdk) repository; this code was previously in a monorepo package there. See also the [JavaScript SDK changelog](https://github.com/launchdarkly/js-client-sdk/blob/main/CHANGELOG.md), since the React SDK inherits all of the underlying functionality of the JavaScript SDK; this file covers only changes that are specific to the React interface. This project adheres to [Semantic Versioning](http://semver.org).
44

55
## [2.26.0] - 2022-04-27
66
### Added:

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# LaunchDarkly Client-side SDK for React
22

3-
[![Circle CI](https://circleci.com/gh/launchdarkly/react-client-sdk/tree/master.svg?style=svg)](https://circleci.com/gh/launchdarkly/react-client-sdk/tree/master)
3+
[![Circle CI](https://circleci.com/gh/launchdarkly/react-client-sdk/tree/main.svg?style=svg)](https://circleci.com/gh/launchdarkly/react-client-sdk/tree/main)
44

55
## LaunchDarkly overview
66

@@ -24,7 +24,7 @@ Please note that the React SDK has two special requirements in terms of your Lau
2424

2525
Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/docs/react-sdk-reference) or our [code-generated API documentation](https://launchdarkly.github.io/react-client-sdk/).
2626

27-
This SDK builds upon the [JavaScript SDK](https://github.com/launchdarkly/js-client-sdk), supporting all of the same functionality, but using React's Context API to provide additional conveniences. While using this SDK you may need to directly interact with the underlying JavaScript SDK. For more information on how to use the JavaScript SDK and its characteristics, see the [SDK's README](https://github.com/launchdarkly/js-client-sdk/blob/master/README.md).
27+
This SDK builds upon the [JavaScript SDK](https://github.com/launchdarkly/js-client-sdk), supporting all of the same functionality, but using React's Context API to provide additional conveniences. While using this SDK you may need to directly interact with the underlying JavaScript SDK. For more information on how to use the JavaScript SDK and its characteristics, see the [SDK's README](https://github.com/launchdarkly/js-client-sdk/blob/main/README.md).
2828

2929
## Testing
3030

examples/async-provider/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
],
2424
"author": "LaunchDarkly <[email protected]>",
2525
"license": "Apache-2.0",
26-
"homepage": "https://github.com/launchdarkly/js-client-sdk/tree/master/packages/launchdarkly-react-client-sdk",
26+
"homepage": "https://github.com/launchdarkly/js-client-sdk/tree/main/packages/launchdarkly-react-client-sdk",
2727
"dependencies": {
2828
"@babel/polyfill": "^7.2.5",
2929
"express": "^4.16.4",

examples/hoc/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
],
2424
"author": "LaunchDarkly <[email protected]>",
2525
"license": "Apache-2.0",
26-
"homepage": "https://github.com/launchdarkly/js-client-sdk/tree/master/packages/launchdarkly-react-client-sdk",
26+
"homepage": "https://github.com/launchdarkly/js-client-sdk/tree/main/packages/launchdarkly-react-client-sdk",
2727
"dependencies": {
2828
"@babel/polyfill": "^7.2.5",
2929
"express": "^4.16.4",

package.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
"@types/react": "^18.0.3",
4747
"@types/react-dom": "^18.0.0",
4848
"@types/react-test-renderer": "^18.0.0",
49-
"@types/uuid": "^3.4.5",
5049
"jest": "^27.4.4",
50+
"jest-environment-jsdom": "^27.4.4",
5151
"jest-environment-jsdom-global": "^3.0.0",
5252
"jest-junit": "^13.0.0",
5353
"prettier": "^1.18.2",
@@ -65,8 +65,7 @@
6565
"dependencies": {
6666
"hoist-non-react-statics": "^3.3.2",
6767
"launchdarkly-js-client-sdk": "2.22.1",
68-
"lodash.camelcase": "^4.3.0",
69-
"uuid": "^3.3.2"
68+
"lodash.camelcase": "^4.3.0"
7069
},
7170
"peerDependencies": {
7271
"react": "^16.6.3 || ^17.0.0 || ^18.0.0",

src/__snapshots__/provider.test.tsx.snap

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ exports[`LDProvider render app 1`] = `
44
<Provider
55
value={
66
Object {
7+
"error": undefined,
8+
"flagKeyMap": Object {},
79
"flags": Object {},
810
"ldClient": undefined,
911
}

src/__snapshots__/withLDProvider.test.tsx.snap

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ exports[`withLDProvider render app 1`] = `
44
<Provider
55
value={
66
Object {
7+
"error": undefined,
8+
"flagKeyMap": Object {},
79
"flags": Object {},
810
"ldClient": undefined,
911
}

src/asyncWithLDProvider.test.tsx

+18-18
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,16 @@ import React from 'react';
1212
import { render } from '@testing-library/react';
1313
import { LDFlagChangeset, LDOptions, LDUser } from 'launchdarkly-js-client-sdk';
1414
import initLDClient from './initLDClient';
15-
import { fetchFlags } from './utils';
16-
import { AsyncProviderConfig, defaultReactOptions, LDReactOptions } from './types';
15+
import { AsyncProviderConfig, LDReactOptions } from './types';
1716
import { Consumer } from './context';
1817
import asyncWithLDProvider from './asyncWithLDProvider';
1918

2019
const clientSideID = 'deadbeef';
2120
const user: LDUser = { key: 'yus', name: 'yus ng' };
2221
const App = () => <>My App</>;
2322
const mockInitLDClient = initLDClient as jest.Mock;
24-
const mockFetchFlags = fetchFlags as jest.Mock;
25-
const mockFlags = { testFlag: true, anotherTestFlag: true };
26-
let mockLDClient: { on: jest.Mock };
23+
const rawFlags = { 'test-flag': true, 'another-test-flag': true };
24+
let mockLDClient: { on: jest.Mock; off: jest.Mock; variation: jest.Mock };
2725

2826
const renderWithConfig = async (config: AsyncProviderConfig) => {
2927
const LDProvider = await asyncWithLDProvider(config);
@@ -43,13 +41,15 @@ describe('asyncWithLDProvider', () => {
4341
on: jest.fn((e: string, cb: () => void) => {
4442
cb();
4543
}),
44+
off: jest.fn(),
45+
// tslint:disable-next-line: no-unsafe-any
46+
variation: jest.fn((_: string, v) => v),
4647
};
4748

4849
mockInitLDClient.mockImplementation(() => ({
4950
ldClient: mockLDClient,
51+
flags: rawFlags,
5052
}));
51-
52-
mockFetchFlags.mockReturnValue(mockFlags);
5353
});
5454

5555
afterEach(() => {
@@ -72,7 +72,7 @@ describe('asyncWithLDProvider', () => {
7272
const reactOptions: LDReactOptions = { useCamelCaseFlagKeys: false };
7373
await asyncWithLDProvider({ clientSideID, user, options, reactOptions });
7474

75-
expect(mockInitLDClient).toHaveBeenCalledWith(clientSideID, user, reactOptions, options, undefined);
75+
expect(mockInitLDClient).toHaveBeenCalledWith(clientSideID, user, options, undefined);
7676
});
7777

7878
test('subscribe to changes on mount', async () => {
@@ -97,17 +97,17 @@ describe('asyncWithLDProvider', () => {
9797
});
9898

9999
test('subscribe to changes with kebab-case', async () => {
100-
mockFetchFlags.mockReturnValue({ 'another-test-flag': true, 'test-flag': true });
101100
mockInitLDClient.mockImplementation(() => ({
102101
ldClient: mockLDClient,
102+
flags: rawFlags,
103103
}));
104104
mockLDClient.on.mockImplementation((e: string, cb: (c: LDFlagChangeset) => void) => {
105105
cb({ 'another-test-flag': { current: false, previous: true }, 'test-flag': { current: false, previous: true } });
106106
});
107107
const receivedNode = await renderWithConfig({ clientSideID, reactOptions: { useCamelCaseFlagKeys: false } });
108108

109109
expect(mockLDClient.on).toHaveBeenNthCalledWith(1, 'change', expect.any(Function));
110-
expect(receivedNode).toHaveTextContent('{"another-test-flag":false,"test-flag":false}');
110+
expect(receivedNode).toHaveTextContent('{"test-flag":false,"another-test-flag":false}');
111111
});
112112

113113
test('consecutive flag changes gets stored in context correctly', async () => {
@@ -180,31 +180,31 @@ describe('asyncWithLDProvider', () => {
180180
});
181181

182182
test('ldClient is initialised correctly with target flags', async () => {
183-
mockFetchFlags.mockReturnValue({ devTestFlag: true, launchDoggly: true });
184183
mockInitLDClient.mockImplementation(() => ({
185184
ldClient: mockLDClient,
185+
flags: rawFlags,
186186
}));
187187

188188
const options: LDOptions = {};
189-
const flags = { 'dev-test-flag': false, 'launch-doggly': false };
189+
const flags = { 'test-flag': false };
190190
const receivedNode = await renderWithConfig({ clientSideID, user, options, flags });
191191

192-
expect(mockInitLDClient).toHaveBeenCalledWith(clientSideID, user, defaultReactOptions, options, flags);
193-
expect(receivedNode).toHaveTextContent('{"devTestFlag":true,"launchDoggly":true}');
192+
expect(mockInitLDClient).toHaveBeenCalledWith(clientSideID, user, options, flags);
193+
expect(receivedNode).toHaveTextContent('{"testFlag":true}');
194194
});
195195

196196
test('only updates to subscribed flags are pushed to the Provider', async () => {
197-
mockFetchFlags.mockReturnValue({ testFlag: 2 });
198197
mockInitLDClient.mockImplementation(() => ({
199198
ldClient: mockLDClient,
199+
flags: rawFlags,
200200
}));
201201
mockLDClient.on.mockImplementation((e: string, cb: (c: LDFlagChangeset) => void) => {
202-
cb({ 'test-flag': { current: 3, previous: 2 }, 'another-test-flag': { current: false, previous: true } });
202+
cb({ 'test-flag': { current: false, previous: true }, 'another-test-flag': { current: false, previous: true } });
203203
});
204204
const options: LDOptions = {};
205-
const subscribedFlags = { 'test-flag': 1 };
205+
const subscribedFlags = { 'test-flag': true };
206206
const receivedNode = await renderWithConfig({ clientSideID, user, options, flags: subscribedFlags });
207207

208-
expect(receivedNode).toHaveTextContent('{"testFlag":3}');
208+
expect(receivedNode).toHaveTextContent('{"testFlag":false}');
209209
});
210210
});

src/asyncWithLDProvider.tsx

+29-17
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import React, { useState, useEffect, ReactNode } from 'react';
2-
import { LDFlagSet, LDFlagChangeset } from 'launchdarkly-js-client-sdk';
2+
import { LDFlagChangeset, LDFlagSet } from 'launchdarkly-js-client-sdk';
33
import { AsyncProviderConfig, defaultReactOptions } from './types';
44
import { Provider } from './context';
55
import initLDClient from './initLDClient';
6-
import { camelCaseKeys, fetchFlags, getFlattenedFlagsFromChangeset } from './utils';
6+
import { getFlattenedFlagsFromChangeset } from './utils';
7+
import getFlagsProxy from './getFlagsProxy';
78

89
/**
910
* This is an async function which initializes LaunchDarkly's JS SDK (`launchdarkly-js-client-sdk`)
@@ -32,32 +33,43 @@ import { camelCaseKeys, fetchFlags, getFlattenedFlagsFromChangeset } from './uti
3233
export default async function asyncWithLDProvider(config: AsyncProviderConfig) {
3334
const { clientSideID, user, flags: targetFlags, options, reactOptions: userReactOptions } = config;
3435
const reactOptions = { ...defaultReactOptions, ...userReactOptions };
35-
const { ldClient } = await initLDClient(clientSideID, user, reactOptions, options, targetFlags);
36+
const { ldClient, flags: fetchedFlags, error } = await initLDClient(clientSideID, user, options, targetFlags);
3637

3738
const LDProvider = ({ children }: { children: ReactNode }) => {
3839
const [ldData, setLDData] = useState({
39-
flags: fetchFlags(ldClient, reactOptions, targetFlags),
40-
ldClient,
40+
flags: {},
41+
unproxiedFlags: {},
42+
flagKeyMap: {},
4143
});
4244

4345
useEffect(() => {
44-
if (options) {
45-
const { bootstrap } = options;
46-
if (bootstrap && bootstrap !== 'localStorage') {
47-
const bootstrappedFlags = reactOptions.useCamelCaseFlagKeys ? camelCaseKeys(bootstrap) : bootstrap;
48-
setLDData((prev) => ({ ...prev, flags: bootstrappedFlags }));
46+
const initialFlags =
47+
options?.bootstrap && options.bootstrap !== 'localStorage' ? options.bootstrap : fetchedFlags;
48+
setLDData({ unproxiedFlags: initialFlags, ...getFlagsProxy(ldClient, initialFlags, reactOptions, targetFlags) });
49+
50+
function onChange(changes: LDFlagChangeset) {
51+
const updates = getFlattenedFlagsFromChangeset(changes, targetFlags);
52+
if (Object.keys(updates).length > 0) {
53+
setLDData(({ unproxiedFlags }) => {
54+
const updatedUnproxiedFlags = { ...unproxiedFlags, ...updates };
55+
56+
return {
57+
unproxiedFlags: updatedUnproxiedFlags,
58+
...getFlagsProxy(ldClient, updatedUnproxiedFlags, reactOptions, targetFlags),
59+
};
60+
});
4961
}
5062
}
63+
ldClient.on('change', onChange);
5164

52-
ldClient.on('change', (changes: LDFlagChangeset) => {
53-
const flattened: LDFlagSet = getFlattenedFlagsFromChangeset(changes, targetFlags, reactOptions);
54-
if (Object.keys(flattened).length > 0) {
55-
setLDData((prev) => ({ ...prev, flags: { ...prev.flags, ...flattened } }));
56-
}
57-
});
65+
return function cleanup() {
66+
ldClient.off('change', onChange);
67+
};
5868
}, []);
5969

60-
return <Provider value={ldData}>{children}</Provider>;
70+
const { flags, flagKeyMap } = ldData;
71+
72+
return <Provider value={{ flags, flagKeyMap, ldClient, error }}>{children}</Provider>;
6173
};
6274

6375
return LDProvider;

src/context.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,41 @@
11
import { createContext } from 'react';
22
import { LDClient, LDFlagSet } from 'launchdarkly-js-client-sdk';
3+
import { LDFlagKeyMap } from './types';
34

45
/**
56
* The LaunchDarkly context stored in the Provider state and passed to consumers.
67
*/
78
interface LDContext {
89
/**
9-
* Contains all flags from LaunchDarkly. This object will always exist but will be empty {} initially
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
1012
* until flags are fetched from the LaunchDarkly servers.
1113
*/
1214
flags: LDFlagSet;
1315

16+
/**
17+
* Map of camelized flag keys to their original unmodified form. Empty if useCamelCaseFlagKeys option is false.
18+
*/
19+
flagKeyMap: LDFlagKeyMap;
20+
1421
/**
1522
* An instance of `LDClient` from the LaunchDarkly JS SDK (`launchdarkly-js-client-sdk`).
1623
* This will be be undefined initially until initialization is complete.
1724
*
1825
* @see https://docs.launchdarkly.com/sdk/client-side/javascript
1926
*/
2027
ldClient?: LDClient;
28+
29+
/**
30+
* LaunchDarkly client initialization error, if there was one.
31+
*/
32+
error?: Error;
2133
}
2234

2335
/**
2436
* @ignore
2537
*/
26-
const context = createContext<LDContext>({ flags: {}, ldClient: undefined });
38+
const context = createContext<LDContext>({ flags: {}, flagKeyMap: {}, ldClient: undefined });
2739
const {
2840
/**
2941
* @ignore

src/getFlagsProxy.test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { LDClient, LDFlagSet } from 'launchdarkly-js-client-sdk';
2+
import getFlagsProxy from './getFlagsProxy';
3+
import { defaultReactOptions } from './types';
4+
5+
// tslint:disable-next-line: no-unsafe-any
6+
const variation = jest.fn((k: string): string | undefined => rawFlags[k]);
7+
8+
const ldClient = ({ variation } as unknown) as LDClient;
9+
10+
const rawFlags: LDFlagSet = {
11+
'foo-bar': 'foobar',
12+
'baz-qux': 'bazqux',
13+
};
14+
15+
const camelizedFlags: LDFlagSet = {
16+
fooBar: 'foobar',
17+
bazQux: 'bazqux',
18+
};
19+
20+
beforeEach(jest.clearAllMocks);
21+
22+
test('camel cases keys', () => {
23+
const { flags } = getFlagsProxy(ldClient, rawFlags);
24+
25+
expect(flags).toEqual(camelizedFlags);
26+
});
27+
28+
test('does not camel cases keys', () => {
29+
const { flags } = getFlagsProxy(ldClient, rawFlags, { useCamelCaseFlagKeys: false });
30+
31+
expect(flags).toEqual(rawFlags);
32+
});
33+
34+
test('proxy calls variation on flag read', () => {
35+
const { flags } = getFlagsProxy(ldClient, rawFlags);
36+
37+
expect(flags.fooBar).toBe('foobar');
38+
39+
expect(variation).toHaveBeenCalledWith('foo-bar', 'foobar');
40+
});
41+
42+
test('returns flag key map', () => {
43+
const { flagKeyMap } = getFlagsProxy(ldClient, rawFlags);
44+
45+
expect(flagKeyMap).toEqual({ fooBar: 'foo-bar', bazQux: 'baz-qux' });
46+
});
47+
48+
test('filters to target flags', () => {
49+
const { flags } = getFlagsProxy(ldClient, rawFlags, defaultReactOptions, { 'foo-bar': 'mr-toot' });
50+
51+
expect(flags).toEqual({ fooBar: 'foobar' });
52+
});
53+
54+
test('does not use proxy if option is false', () => {
55+
const { flags } = getFlagsProxy(ldClient, rawFlags, { sendEventsOnFlagRead: false });
56+
57+
expect(flags['foo-bar']).toBe('foobar');
58+
59+
expect(variation).not.toHaveBeenCalled();
60+
});

0 commit comments

Comments
 (0)