Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v1.10.1 #173

Merged
merged 7 commits into from
Nov 21, 2023
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
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
1.10.1 (November 21, 2023)
- Bugfixing - Resolved an issue with `useSplitClient` hook, which was not re-rendering when an SDK client event was emitted between the hook's call (render phase) and the execution of its internal effect (commit phase).

1.10.0 (November 16, 2023)
- Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation):
- Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` option to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-react",
"version": "1.10.0",
"version": "1.10.1",
"description": "A React library to easily integrate and use Split JS SDK",
"main": "lib/index.js",
"module": "es/index.js",
Expand Down
45 changes: 22 additions & 23 deletions src/SplitClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,37 +63,36 @@ export class SplitComponent extends React.Component<IUpdateProps & { factory: Sp
// The listeners take into account the value of `updateOnSdk***` props.
subscribeToEvents(client: SplitIO.IBrowserClient | null) {
if (client) {
const status = getStatus(client);
if (!status.isReady) client.once(client.Event.SDK_READY, this.setReady);
if (!status.isReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, this.setReadyFromCache);
if (!status.hasTimedout && !status.isReady) client.once(client.Event.SDK_READY_TIMED_OUT, this.setTimedout);
client.on(client.Event.SDK_UPDATE, this.setUpdate);
const statusOnEffect = getStatus(client);
const status = this.state;

if (this.props.updateOnSdkReady) {
if (!statusOnEffect.isReady) client.once(client.Event.SDK_READY, this.update);
else if (!status.isReady) this.update();
}
if (this.props.updateOnSdkReadyFromCache) {
if (!statusOnEffect.isReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, this.update);
else if (!status.isReadyFromCache) this.update();
}
if (this.props.updateOnSdkTimedout) {
if (!statusOnEffect.hasTimedout) client.once(client.Event.SDK_READY_TIMED_OUT, this.update);
else if (!status.hasTimedout) this.update();
}
if (this.props.updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, this.update);
}
}

unsubscribeFromEvents(client: SplitIO.IBrowserClient | null) {
if (client) {
client.removeListener(client.Event.SDK_READY, this.setReady);
client.removeListener(client.Event.SDK_READY_FROM_CACHE, this.setReadyFromCache);
client.removeListener(client.Event.SDK_READY_TIMED_OUT, this.setTimedout);
client.removeListener(client.Event.SDK_UPDATE, this.setUpdate);
client.off(client.Event.SDK_READY, this.update);
client.off(client.Event.SDK_READY_FROM_CACHE, this.update);
client.off(client.Event.SDK_READY_TIMED_OUT, this.update);
client.off(client.Event.SDK_UPDATE, this.update);
}
}

setReady = () => {
if (this.props.updateOnSdkReady) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate });
}

setReadyFromCache = () => {
if (this.props.updateOnSdkReadyFromCache) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate });
}

setTimedout = () => {
if (this.props.updateOnSdkTimedout) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate });
}

setUpdate = () => {
if (this.props.updateOnSdkUpdate) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate });
update = () => {
this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate });
}

componentDidMount() {
Expand Down
47 changes: 38 additions & 9 deletions src/__tests__/useSplitClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe('useSplitClient', () => {

// eslint-disable-next-line react/prop-types
const InnerComponent = ({ splitKey, attributesClient, testSwitch }) => {
useSplitClient({ splitKey, trafficType: 'user', attributes: attributesClient});
useSplitClient({ splitKey, trafficType: 'user', attributes: attributesClient });
testSwitch(done, splitKey);
return null;
};
Expand All @@ -98,13 +98,13 @@ describe('useSplitClient', () => {
testAttributesBinding(Component);
});

test('useSplitClient must update on SDK events', () => {
test('must update on SDK events', () => {
const outerFactory = SplitSdk(sdkBrowser);
const mainClient = outerFactory.client() as any;
const user2Client = outerFactory.client('user_2') as any;

let countSplitContext = 0, countSplitClient = 0, countSplitClientUser2 = 0, countUseSplitClient = 0, countUseSplitClientUser2 = 0;
let countSplitClientWithUpdate = 0, countUseSplitClientWithUpdate = 0, countSplitClientUser2WithUpdate = 0, countUseSplitClientUser2WithUpdate = 0;
let countSplitClientWithUpdate = 0, countUseSplitClientWithUpdate = 0, countSplitClientUser2WithUpdate = 0, countUseSplitClientUser2WithTimeout = 0;
let countNestedComponent = 0;

render(
Expand Down Expand Up @@ -150,8 +150,8 @@ describe('useSplitClient', () => {
{() => { countSplitClientUser2WithUpdate++; return null }}
</SplitClient>
{React.createElement(() => {
useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true });
countUseSplitClientUser2WithUpdate++;
useSplitClient({ splitKey: 'user_2', updateOnSdkTimedout: true });
countUseSplitClientUser2WithTimeout++;
return null;
})}
<SplitClient splitKey={'user_2'} updateOnSdkUpdate={true}>
Expand Down Expand Up @@ -190,6 +190,7 @@ describe('useSplitClient', () => {
act(() => mainClient.__emitter__.emit(Event.SDK_READY));
act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE));
act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
act(() => user2Client.__emitter__.emit(Event.SDK_READY_TIMED_OUT));
act(() => user2Client.__emitter__.emit(Event.SDK_READY));
act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE));

Expand All @@ -214,12 +215,35 @@ describe('useSplitClient', () => {
// If SplitClient and useSplitClient retrieve a different client than the context and have updateOnSdkUpdate = true,
// they render when the context renders and when the new client is ready, ready from cache and updates.
expect(countSplitClientUser2WithUpdate).toEqual(countSplitContext + 3);
expect(countUseSplitClientUser2WithUpdate).toEqual(countSplitContext + 3);
expect(countUseSplitClientUser2WithTimeout).toEqual(countSplitContext + 3);

expect(countNestedComponent).toEqual(4);
});

test('useSplitClient must support changes in update props', () => {
// Remove this test once side effects are moved to the useSplitClient effect.
test('must update on SDK events between the render phase (hook call) and commit phase (effect call)', () =>{
const outerFactory = SplitSdk(sdkBrowser);
let count = 0;

render(
<SplitFactory factory={outerFactory} >
{React.createElement(() => {
useSplitClient({ splitKey: 'some_user' });
count++;

// side effect in the render phase
const client = outerFactory.client('some_user') as any;
if (!client.__getStatus().isReady) client.__emitter__.emit(Event.SDK_READY);

return null;
})}
</SplitFactory>
)

expect(count).toEqual(2);
});

test('must support changes in update props', () => {
const outerFactory = SplitSdk(sdkBrowser);
const mainClient = outerFactory.client() as any;

Expand All @@ -240,19 +264,24 @@ describe('useSplitClient', () => {
}

const wrapper = render(<Component />);
expect(rendersCount).toBe(1);

act(() => mainClient.__emitter__.emit(Event.SDK_READY)); // trigger re-render
expect(rendersCount).toBe(2);

act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false by default
expect(rendersCount).toBe(2);

wrapper.rerender(<Component updateOnSdkUpdate={true} />); // trigger re-render
act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // trigger re-render because updateOnSdkUpdate is true now
expect(rendersCount).toBe(3);

act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // trigger re-render because updateOnSdkUpdate is true now
expect(rendersCount).toBe(4);

wrapper.rerender(<Component updateOnSdkUpdate={false} />); // trigger re-render
act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false now
expect(rendersCount).toBe(5);

act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false now
expect(rendersCount).toBe(5);
});

Expand Down
27 changes: 20 additions & 7 deletions src/useSplitClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,36 @@ export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextV

let client = contextClient as IClientWithContext;
if (splitKey && factory) {
// @TODO `getSplitClient` starts client sync. Move side effects to useEffect
client = getSplitClient(factory, splitKey, trafficType);
}
initAttributes(client, attributes);

const [, setLastUpdate] = React.useState(client ? client.lastUpdate : 0);
const status = getStatus(client);
const [, setLastUpdate] = React.useState(status.lastUpdate);

// Handle client events
React.useEffect(() => {
if (!client) return;

const update = () => setLastUpdate(client.lastUpdate);

// Clients are created on the hook's call, so the status may have changed
const statusOnEffect = getStatus(client);

// Subscribe to SDK events
const status = getStatus(client);
if (!status.isReady && updateOnSdkReady) client.once(client.Event.SDK_READY, update);
if (!status.isReadyFromCache && updateOnSdkReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, update);
if (!status.hasTimedout && !status.isReady && updateOnSdkTimedout) client.once(client.Event.SDK_READY_TIMED_OUT, update);
if (updateOnSdkReady) {
if (!statusOnEffect.isReady) client.once(client.Event.SDK_READY, update);
else if (!status.isReady) update();
}
if (updateOnSdkReadyFromCache) {
if (!statusOnEffect.isReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, update);
else if (!status.isReadyFromCache) update();
}
if (updateOnSdkTimedout) {
if (!statusOnEffect.hasTimedout) client.once(client.Event.SDK_READY_TIMED_OUT, update);
else if (!status.hasTimedout) update();
}
if (updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, update);

return () => {
Expand All @@ -59,9 +72,9 @@ export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextV
client.off(client.Event.SDK_READY_TIMED_OUT, update);
client.off(client.Event.SDK_UPDATE, update);
}
}, [client, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate]);
}, [client, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate, status]);

return {
factory, client, ...getStatus(client)
factory, client, ...status
};
}