Skip to content

Commit 2086a07

Browse files
committed
fix: react context no longer changes on flag change
1 parent 7f7fd65 commit 2086a07

File tree

5 files changed

+117
-116
lines changed

5 files changed

+117
-116
lines changed

packages/sdk/react-native/__tests__/provider/LDProvider.test.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { render } from '@testing-library/react';
2+
import React from 'react';
23

34
import { AutoEnvAttributes, LDContext, LDOptions } from '@launchdarkly/js-client-sdk-common';
45

56
import { useLDClient } from '../../src/hooks';
67
import LDProvider from '../../src/provider/LDProvider';
7-
import setupListeners from '../../src/provider/setupListeners';
88
import ReactNativeLDClient from '../../src/ReactNativeLDClient';
99

1010
jest.mock('../../src/ReactNativeLDClient');
11-
jest.mock('../../src/provider/setupListeners');
1211

1312
const TestApp = () => {
1413
const ldClient = useLDClient();
@@ -22,7 +21,6 @@ const TestApp = () => {
2221
};
2322
describe('LDProvider', () => {
2423
let ldc: ReactNativeLDClient;
25-
const mockSetupListeners = setupListeners as jest.Mock;
2624

2725
beforeEach(() => {
2826
jest.useFakeTimers();
@@ -45,9 +43,7 @@ describe('LDProvider', () => {
4543
};
4644
},
4745
);
48-
mockSetupListeners.mockImplementation((client: ReactNativeLDClient, setState: any) => {
49-
setState({ client });
50-
});
46+
5147
ldc = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled);
5248
});
5349

packages/sdk/react-native/__tests__/provider/setupListeners.test.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 108 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
3+
import type ReactNativeLDClient from '../../ReactNativeLDClient';
14
import useLDClient from '../useLDClient';
25
import { LDEvaluationDetailTyped } from './LDEvaluationDetail';
36

7+
function getTypedVariation<T extends boolean | number | string | unknown>(
8+
ldClient: ReactNativeLDClient,
9+
key: string,
10+
defaultValue: T,
11+
): T {
12+
switch (typeof defaultValue) {
13+
case 'boolean':
14+
return ldClient.boolVariation(key, defaultValue as boolean) as T;
15+
case 'number':
16+
return ldClient.numberVariation(key, defaultValue as number) as T;
17+
case 'string':
18+
return ldClient.stringVariation(key, defaultValue as string) as T;
19+
case 'undefined':
20+
case 'object':
21+
return ldClient.jsonVariation(key, defaultValue) as T;
22+
default:
23+
return ldClient.variation(key, defaultValue);
24+
}
25+
}
26+
427
/**
528
* Determines the strongly typed variation of a feature flag.
629
*
@@ -15,21 +38,72 @@ export const useTypedVariation = <T extends boolean | number | string | unknown>
1538
defaultValue: T,
1639
): T => {
1740
const ldClient = useLDClient();
41+
const [value, setValue] = useState<T>(() =>
42+
ldClient ? getTypedVariation(ldClient, key, defaultValue) : defaultValue,
43+
);
44+
const valueRef = useRef<T>(value);
45+
46+
useEffect(() => {
47+
valueRef.current = value;
48+
}, [value]);
49+
50+
useEffect(() => {
51+
setValue(getTypedVariation(ldClient, key, defaultValue));
52+
const handleChange = (): void => {
53+
const newValue = getTypedVariation(ldClient, key, defaultValue);
54+
if (newValue !== valueRef.current) {
55+
setValue(newValue);
56+
}
57+
};
58+
ldClient.on('change', handleChange);
59+
return () => {
60+
ldClient.off('change', handleChange);
61+
};
62+
}, [key]);
1863

64+
return value;
65+
};
66+
67+
function getTypedVariationDetail<T extends boolean | number | string | unknown>(
68+
ldClient: ReactNativeLDClient,
69+
key: string,
70+
defaultValue: T,
71+
): LDEvaluationDetailTyped<T> {
72+
let detail: LDEvaluationDetailTyped<T>;
1973
switch (typeof defaultValue) {
20-
case 'boolean':
21-
return ldClient.boolVariation(key, defaultValue as boolean) as T;
22-
case 'number':
23-
return ldClient.numberVariation(key, defaultValue as number) as T;
24-
case 'string':
25-
return ldClient.stringVariation(key, defaultValue as string) as T;
74+
case 'boolean': {
75+
detail = ldClient.boolVariationDetail(
76+
key,
77+
defaultValue as boolean,
78+
) as LDEvaluationDetailTyped<T>;
79+
break;
80+
}
81+
case 'number': {
82+
detail = ldClient.numberVariationDetail(
83+
key,
84+
defaultValue as number,
85+
) as LDEvaluationDetailTyped<T>;
86+
break;
87+
}
88+
case 'string': {
89+
detail = ldClient.stringVariationDetail(
90+
key,
91+
defaultValue as string,
92+
) as LDEvaluationDetailTyped<T>;
93+
break;
94+
}
2695
case 'undefined':
27-
case 'object':
28-
return ldClient.jsonVariation(key, defaultValue) as T;
29-
default:
30-
return ldClient.variation(key, defaultValue);
96+
case 'object': {
97+
detail = ldClient.jsonVariationDetail(key, defaultValue) as LDEvaluationDetailTyped<T>;
98+
break;
99+
}
100+
default: {
101+
detail = ldClient.variationDetail(key, defaultValue) as LDEvaluationDetailTyped<T>;
102+
break;
103+
}
31104
}
32-
};
105+
return { ...detail, reason: detail.reason ?? null };
106+
}
33107

34108
/**
35109
* Determines the strongly typed variation of a feature flag for a context, along with information about
@@ -55,48 +129,30 @@ export const useTypedVariationDetail = <T extends boolean | number | string | un
55129
defaultValue: T,
56130
): LDEvaluationDetailTyped<T> => {
57131
const ldClient = useLDClient();
132+
const [detail, setDetail] = useState<LDEvaluationDetailTyped<T>>(() =>
133+
ldClient
134+
? getTypedVariationDetail(ldClient, key, defaultValue)
135+
: { value: defaultValue, reason: null },
136+
);
137+
const detailRef = useRef<LDEvaluationDetailTyped<T>>(detail);
58138

59-
switch (typeof defaultValue) {
60-
case 'boolean': {
61-
const detail = ldClient.boolVariationDetail(key, defaultValue as boolean);
62-
63-
return {
64-
...detail,
65-
reason: detail.reason ?? null,
66-
} as LDEvaluationDetailTyped<T>;
67-
}
68-
case 'number': {
69-
const detail = ldClient.numberVariationDetail(key, defaultValue as number);
139+
useEffect(() => {
140+
detailRef.current = detail;
141+
}, [detail]);
70142

71-
return {
72-
...detail,
73-
reason: detail.reason ?? null,
74-
} as LDEvaluationDetailTyped<T>;
75-
}
76-
case 'string': {
77-
const detail = ldClient.stringVariationDetail(key, defaultValue as string);
143+
useEffect(() => {
144+
setDetail(getTypedVariationDetail(ldClient, key, defaultValue));
145+
const handleChange = () => {
146+
const newDetail = getTypedVariationDetail(ldClient, key, defaultValue);
147+
if (newDetail.value !== detailRef.current.value) {
148+
setDetail(newDetail);
149+
}
150+
};
151+
ldClient.on('change', handleChange);
152+
return () => {
153+
ldClient.off('change', handleChange);
154+
};
155+
}, [key]);
78156

79-
return {
80-
...detail,
81-
reason: detail.reason ?? null,
82-
} as LDEvaluationDetailTyped<T>;
83-
}
84-
case 'undefined':
85-
case 'object': {
86-
const detail = ldClient.jsonVariationDetail(key, defaultValue);
87-
88-
return {
89-
...detail,
90-
reason: detail.reason ?? null,
91-
} as LDEvaluationDetailTyped<T>;
92-
}
93-
default: {
94-
const detail = ldClient.variationDetail(key, defaultValue);
95-
96-
return {
97-
...detail,
98-
reason: detail.reason ?? null,
99-
} as LDEvaluationDetailTyped<T>;
100-
}
101-
}
157+
return detail;
102158
};
Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import React, { PropsWithChildren, useEffect, useState } from 'react';
1+
import { PropsWithChildren, useMemo } from 'react';
22

33
import ReactNativeLDClient from '../ReactNativeLDClient';
4-
import { Provider, ReactContext } from './reactContext';
5-
import setupListeners from './setupListeners';
4+
import { Provider } from './reactContext';
65

76
type LDProps = {
87
client: ReactNativeLDClient;
@@ -19,13 +18,12 @@ type LDProps = {
1918
* @constructor
2019
*/
2120
const LDProvider = ({ client, children }: PropsWithChildren<LDProps>) => {
22-
const [state, setState] = useState<ReactContext>({ client });
21+
// NOTE: this could only provide marginal benefits, if the provider is
22+
// a child component of a parent that is re-rendering then this
23+
// may still re-render the context value.
24+
const clientContext = useMemo(() => ({ client }), [client]);
2325

24-
useEffect(() => {
25-
setupListeners(client, setState);
26-
}, []);
27-
28-
return <Provider value={state}>{children}</Provider>;
26+
return <Provider value={clientContext}>{children}</Provider>;
2927
};
3028

3129
export default LDProvider;

packages/sdk/react-native/src/provider/setupListeners.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)