Skip to content

Commit 032d849

Browse files
Merge branch 'hooks_baseline' into hooks_update_options
2 parents 567ed42 + 0fce4b5 commit 032d849

14 files changed

Lines changed: 476 additions & 527 deletions

.eslintrc.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ module.exports = {
2929
'eol-last': ['error', 'always'],
3030
'keyword-spacing': 'error',
3131
'no-trailing-spaces': 'error',
32-
'space-before-function-paren': ['error', {'named': 'never'}],
32+
'space-before-function-paren': ['error', { 'named': 'never' }],
3333
'react/display-name': 'off',
3434
'@typescript-eslint/no-empty-function': 'off',
3535
'@typescript-eslint/no-inferrable-types': 'off',
@@ -40,6 +40,11 @@ module.exports = {
4040
'destructuring': 'all'
4141
}]
4242
},
43+
'settings': {
44+
'react': {
45+
'version': '16.3.0' // minimum supported version of React
46+
}
47+
},
4348
'overrides': [{
4449
'files': ['src/**/*.ts', 'src/**/*.tsx'],
4550
'excludedFiles': ['src/**/__tests__/**'],
@@ -53,7 +58,7 @@ module.exports = {
5358
},
5459
'rules': {
5560
'no-restricted-syntax': ['error', 'ForOfStatement', 'ForInStatement'],
56-
'compat/compat': ['error', 'defaults, not ie < 11'],
61+
'compat/compat': ['error', 'defaults, ie 11'],
5762
'no-throw-literal': 'error',
5863
'import/no-self-import': 'error',
5964
'import/no-default-export': 'error',

CHANGES.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
1.10.0 (XXX XX, 2023)
2-
- Bugfixing - Removed check of hooks availability to follow rules of hooks. If attempting to use hooks with React below version 16.8.0, an error will be thrown rather than logging an error message.
1+
1.10.0 (September XX, 2023)
2+
- Added TypeScript types and interfaces to the library index exports, allowing them to be imported from the library index. For example, `import type { ISplitFactoryProps } from '@splitsoftware/splitio-react';` (Related to issue https://github.com/splitio/react-client/issues/162).
3+
- Updated linter and other dependencies for vulnerability fixes.
4+
- Bugfixing - To adhere to the rules of hooks and prevent React warnings, conditional code within hooks was removed. Previously, this code checked for the availability of the hooks API (available in React version 16.8.0 or above) and logged an error message. Now, using hooks with React versions below 16.8.0 will throw an error.
5+
- Bugfixing - Updated `useClient` and `useTreatments` hooks to re-render and re-evaluate feature flags when they consume a different SDK client than the context and its status updates (i.e., when it emits SDK_READY or other event).
36

47
1.9.0 (July 18, 2023)
58
- Updated some transitive dependencies for vulnerability fixes.

package-lock.json

Lines changed: 378 additions & 455 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@
7575
"@types/react-dom": "^18.0.0",
7676
"@types/react-test-renderer": "^18.0.0",
7777
"@types/shallowequal": "^1.1.1",
78-
"@typescript-eslint/eslint-plugin": "^5.55.0",
79-
"@typescript-eslint/parser": "^5.55.0",
80-
"eslint": "^8.36.0",
81-
"eslint-plugin-compat": "^4.1.2",
78+
"@typescript-eslint/eslint-plugin": "^6.6.0",
79+
"@typescript-eslint/parser": "^6.6.0",
80+
"eslint": "^8.48.0",
81+
"eslint-plugin-compat": "^4.2.0",
8282
"eslint-plugin-import": "^2.27.5",
8383
"eslint-plugin-react": "^7.32.2",
8484
"eslint-plugin-react-hooks": "^4.6.0",

src/SplitClient.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { SplitContext } from './SplitContext';
33
import { ISplitClientProps, ISplitContextValues, IUpdateProps } from './types';
44
import { ERROR_SC_NO_FACTORY } from './constants';
55
import { getStatus, getSplitClient, initAttributes, IClientWithContext } from './utils';
6+
import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient';
67

78
/**
89
* Common component used to handle the status and events of a Split client passed as prop.
@@ -11,18 +12,15 @@ import { getStatus, getSplitClient, initAttributes, IClientWithContext } from '.
1112
export class SplitComponent extends React.Component<IUpdateProps & { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null, attributes?: SplitIO.Attributes, children: any }, ISplitContextValues> {
1213

1314
static defaultProps = {
14-
updateOnSdkUpdate: false,
15-
updateOnSdkTimedout: false,
16-
updateOnSdkReady: true,
17-
updateOnSdkReadyFromCache: true,
1815
children: null,
1916
factory: null,
2017
client: null,
18+
...DEFAULT_UPDATE_OPTIONS,
2119
}
2220

2321
// Using `getDerivedStateFromProps` since the state depends on the status of the client in props, which might change over time.
2422
// It could be avoided by removing the client and its status from the component state.
25-
// But it implies to have another instance property to use instead of the state, because we need a unique reference value for SplitContext.Producer
23+
// But it implies to have another instance property to use instead of the state, because we need a unique reference value for SplitContext.Provider
2624
static getDerivedStateFromProps(props: ISplitClientProps & { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }, state: ISplitContextValues) {
2725
const { client, factory, attributes } = props;
2826
// initAttributes can be called in the `render` method too, but it is better here for separation of concerns

src/SplitFactory.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { SplitComponent } from './SplitClient';
44
import { ISplitFactoryProps } from './types';
55
import { WARN_SF_CONFIG_AND_FACTORY, ERROR_SF_NO_CONFIG_AND_FACTORY } from './constants';
66
import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient } from './utils';
7+
import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient';
78

89
/**
910
* SplitFactory will initialize the Split SDK and its main client, listen for its events in order to update the Split Context,
@@ -18,11 +19,8 @@ import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClie
1819
export class SplitFactory extends React.Component<ISplitFactoryProps, { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }> {
1920

2021
static defaultProps: ISplitFactoryProps = {
21-
updateOnSdkUpdate: false,
22-
updateOnSdkTimedout: false,
23-
updateOnSdkReady: true,
24-
updateOnSdkReadyFromCache: true,
2522
children: null,
23+
...DEFAULT_UPDATE_OPTIONS,
2624
};
2725

2826
readonly state: Readonly<{ factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }>;

src/__tests__/SplitClient.test.tsx

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -253,36 +253,18 @@ describe('SplitClient', () => {
253253
this.state = { splitKey: 'user1' };
254254
}
255255

256-
componentDidMount() {
257-
setTimeout(() => {
258-
act(() => this.setState({ splitKey: 'user2' }));
259-
setTimeout(() => {
260-
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
261-
setTimeout(() => {
262-
act(() => (outerFactory as any).client('user1').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
263-
setTimeout(() => {
264-
act(() => this.setState({ splitKey: 'user3' }));
265-
setTimeout(() => {
266-
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY));
267-
setTimeout(() => {
268-
act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_READY));
269-
setTimeout(() => {
270-
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE));
271-
setTimeout(() => {
272-
act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_UPDATE));
273-
setTimeout(() => {
274-
expect(renderTimes).toBe(6);
275-
276-
done();
277-
});
278-
});
279-
});
280-
});
281-
});
282-
});
283-
});
284-
});
285-
});
256+
async componentDidMount() {
257+
await act(() => this.setState({ splitKey: 'user2' }));
258+
await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
259+
await act(() => (outerFactory as any).client('user1').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
260+
await act(() => this.setState({ splitKey: 'user3' }));
261+
await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY));
262+
await act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_READY));
263+
await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE));
264+
await act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_UPDATE));
265+
expect(renderTimes).toBe(6);
266+
267+
done();
286268
}
287269

288270
render() {

src/__tests__/SplitTreatments.test.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ describe('SplitTreatments optimization', () => {
224224
expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2);
225225
});
226226

227-
it('rerenders and re-evaluates feature flags if lastUpdate timestamp changes (e.g., SDK_UPDATE event).', (done) => {
227+
it('rerenders and re-evaluates feature flags if lastUpdate timestamp changes (e.g., SDK_UPDATE event).', () => {
228228
expect(renderTimes).toBe(1);
229229

230230
// State update and split evaluation
@@ -234,16 +234,13 @@ describe('SplitTreatments optimization', () => {
234234
(outerFactory as any).client().destroy();
235235
wrapper.rerender(<Component names={names} attributes={attributes} splitKey={splitKey} />);
236236

237-
setTimeout(() => {
238-
// Updates were batched as a single render, due to automatic batching https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching
239-
expect(renderTimes).toBe(3);
240-
expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2);
237+
// Updates were batched as a single render, due to automatic batching https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching
238+
expect(renderTimes).toBe(3);
239+
expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2);
241240

242-
// Restore the client to be READY
243-
(outerFactory as any).client().__restore();
244-
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY);
245-
done();
246-
})
241+
// Restore the client to be READY
242+
(outerFactory as any).client().__restore();
243+
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY);
247244
});
248245

249246
it('rerenders and re-evaluates feature flags if client changes.', () => {
@@ -301,8 +298,6 @@ describe('SplitTreatments optimization', () => {
301298
expect(renderTimesComp1).toBe(2);
302299
expect(renderTimesComp2).toBe(2); // updateOnSdkReadyFromCache === false, in second component
303300

304-
// delay SDK events to guarantee a different lastUpdate timestamp for SplitTreatments to re-evaluate
305-
await new Promise(resolve => setTimeout(resolve, 10));
306301
act(() => {
307302
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT);
308303
(outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT);
@@ -311,7 +306,6 @@ describe('SplitTreatments optimization', () => {
311306
expect(renderTimesComp1).toBe(3);
312307
expect(renderTimesComp2).toBe(3);
313308

314-
await new Promise(resolve => setTimeout(resolve, 10));
315309
act(() => {
316310
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY);
317311
(outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY);
@@ -320,7 +314,6 @@ describe('SplitTreatments optimization', () => {
320314
expect(renderTimesComp1).toBe(3); // updateOnSdkReady === false, in first component
321315
expect(renderTimesComp2).toBe(4);
322316

323-
await new Promise(resolve => setTimeout(resolve, 10));
324317
act(() => {
325318
(outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE);
326319
(outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE);

src/__tests__/index.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
12
import {
23
SplitContext as ExportedSplitContext,
34
SplitSdk as ExportedSplitSdk,
@@ -11,6 +12,16 @@ import {
1112
useManager as exportedUseManager,
1213
useTrack as exportedUseTrack,
1314
useTreatments as exportedUseTreatments,
15+
// Checks that types are exported. Otherwise, the test would fail with a TS error.
16+
ISplitClientChildProps,
17+
ISplitClientProps,
18+
ISplitContextValues,
19+
ISplitFactoryChildProps,
20+
ISplitFactoryProps,
21+
ISplitStatus,
22+
ISplitTreatmentsChildProps,
23+
ISplitTreatmentsProps,
24+
IUpdateProps
1425
} from '../index';
1526
import { SplitContext } from '../SplitContext';
1627
import { SplitFactory as SplitioEntrypoint } from '@splitsoftware/splitio/client';

src/__tests__/useTreatments.test.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,26 +80,42 @@ describe('useTreatments', () => {
8080
expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments);
8181
});
8282

83-
test('returns the Treatments from a new client given a splitKey, or control if the client is not operational.', async () => {
83+
test('returns the Treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => {
8484
const outerFactory = SplitSdk(sdkBrowser);
8585
const client: any = outerFactory.client('user2');
86-
let treatments;
87-
88-
client.__emitter__.emit(Event.SDK_READY);
89-
await client.destroy();
86+
let renderTimes = 0;
9087

9188
render(
9289
<SplitFactory factory={outerFactory} >
9390
{React.createElement(() => {
94-
treatments = useTreatments(featureFlagNames, attributes, 'user2');
91+
const treatments = useTreatments(featureFlagNames, attributes, 'user2');
92+
93+
renderTimes++;
94+
switch (renderTimes) {
95+
case 1:
96+
// returns control if not operational (SDK not ready), without calling `getTreatmentsWithConfig` method
97+
expect(client.getTreatmentsWithConfig).not.toBeCalled();
98+
expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG });
99+
break;
100+
case 2:
101+
case 3:
102+
// once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags
103+
expect(client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(featureFlagNames, attributes);
104+
expect(client.getTreatmentsWithConfig).toHaveLastReturnedWith(treatments);
105+
break;
106+
default:
107+
throw new Error('Unexpected render');
108+
}
109+
95110
return null;
96111
})}
97112
</SplitFactory>
98113
);
99114

100-
// returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method
101-
expect(client.getTreatmentsWithConfig).not.toBeCalled();
102-
expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG });
115+
act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
116+
act(() => client.__emitter__.emit(Event.SDK_READY));
117+
act(() => client.__emitter__.emit(Event.SDK_UPDATE)); // should not trigger a re-render by default
118+
expect(client.getTreatmentsWithConfig).toBeCalledTimes(2);
103119
});
104120

105121
// THE FOLLOWING TEST WILL PROBABLE BE CHANGED BY 'return a null value or throw an error if it is not inside an SplitProvider'

0 commit comments

Comments
 (0)