Skip to content

Commit c33577f

Browse files
Merge pull request #206 from splitio/error_handling_updates
[Breaking change] Error handling updates
2 parents 7691dd9 + 6d65bf0 commit c33577f

14 files changed

+149
-137
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- Updated @splitsoftware/splitio package to version 10.29.0 that includes minor updates, and updated some transitive dependencies for vulnerability fixes.
44
- Renamed distribution folders from `/lib` to `/cjs` for CommonJS build, and `/es` to `/esm` for EcmaScript Modules build.
55
- BREAKING CHANGES:
6+
- Updated error handling: using the library modules without wrapping them in a `SplitFactoryProvider` component will now throw an error instead of logging it, as the modules requires the `SplitContext` to work properly.
67
- Removed deprecated modules: `SplitFactory` component, `useClient`, `useTreatments` and `useManager` hooks, and `withSplitFactory`, `withSplitClient` and `withSplitTreatments` high-order components. Refer to ./MIGRATION-GUIDE.md for instructions on how to migrate to the new alternatives.
78
- Renamed TypeScript interfaces `ISplitFactoryProps` to `ISplitFactoryProviderProps`, and `ISplitFactoryChildProps` to `ISplitFactoryProviderChildProps`.
89
- Renamed `SplitSdk` to `SplitFactory` function, which is the underlying Split SDK factory, i.e., `import { SplitFactory } from '@splitsoftware/splitio'`.

src/SplitContext.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import React from 'react';
22
import { ISplitContextValues } from './types';
3-
import { EXCEPTION_NO_REACT_OR_CREATECONTEXT } from './constants';
4-
5-
if (!React || !React.createContext) throw new Error(EXCEPTION_NO_REACT_OR_CREATECONTEXT);
6-
7-
export const INITIAL_CONTEXT: ISplitContextValues = {
8-
client: null,
9-
factory: null,
10-
isReady: false,
11-
isReadyFromCache: false,
12-
isTimedout: false,
13-
hasTimedout: false,
14-
lastUpdate: 0,
15-
isDestroyed: false,
16-
}
3+
import { EXCEPTION_NO_SFP } from './constants';
174

185
/**
196
* Split Context is the React Context instance that represents our SplitIO global state.
207
* It contains Split SDK objects, such as a factory instance, a client and its status (isReady, isTimedout, lastUpdate)
218
* The context is created with default empty values, that SplitFactoryProvider and SplitClient access and update.
229
*/
23-
export const SplitContext = React.createContext<ISplitContextValues>(INITIAL_CONTEXT);
10+
export const SplitContext = React.createContext<ISplitContextValues | undefined>(undefined);
11+
12+
/**
13+
* Hook to access the value of `SplitContext`.
14+
*
15+
* @returns The Split Context object value
16+
* @throws Throws an error if the Split Context is not set (i.e. the component is not wrapped in a SplitFactoryProvider)
17+
*/
18+
export function useSplitContext() {
19+
const context = React.useContext(SplitContext);
20+
21+
if (!context) throw new Error(EXCEPTION_NO_SFP)
22+
23+
return context;
24+
}

src/__tests__/SplitClient.test.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { SplitClient } from '../SplitClient';
1616
import { SplitContext } from '../SplitContext';
1717
import { testAttributesBinding, TestComponentProps } from './testUtils/utils';
1818
import { IClientWithContext } from '../utils';
19+
import { EXCEPTION_NO_SFP } from '../constants';
1920

2021
describe('SplitClient', () => {
2122

@@ -254,19 +255,15 @@ describe('SplitClient', () => {
254255
);
255256
});
256257

257-
// @TODO Update test in breaking change, following common practice in React libraries, like React-redux and React-query: use a falsy value as default context value, and throw an error – instead of logging it – if components are not wrapped in a SplitContext.Provider, i.e., if the context is falsy.
258-
// test('logs error and passes null client if rendered outside an SplitProvider component.', () => {
259-
// const errorSpy = jest.spyOn(console, 'error');
260-
// render(
261-
// <SplitClient splitKey='user2' >
262-
// {({ client }) => {
263-
// expect(client).toBe(null);
264-
// return null;
265-
// }}
266-
// </SplitClient>
267-
// );
268-
// expect(errorSpy).toBeCalledWith(ERROR_SC_NO_FACTORY);
269-
// });
258+
test('throws error if invoked outside of SplitFactoryProvider.', () => {
259+
expect(() => {
260+
render(
261+
<SplitClient splitKey='user2' >
262+
{() => null}
263+
</SplitClient>
264+
);
265+
}).toThrow(EXCEPTION_NO_SFP);
266+
});
270267

271268
test(`passes a new client if re-rendered with a different splitKey.
272269
Only updates the state if the new client triggers an event, but not the previous one.`, (done) => {

src/__tests__/SplitContext.test.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React from 'react';
22
import { render } from '@testing-library/react';
33
import { SplitContext } from '../SplitContext';
4+
import { SplitFactoryProvider } from '../SplitFactoryProvider';
5+
import { INITIAL_CONTEXT } from './testUtils/utils';
46

57
/**
68
* Test default SplitContext value
@@ -9,16 +11,22 @@ test('SplitContext.Consumer shows default value', () => {
911
render(
1012
<SplitContext.Consumer>
1113
{(value) => {
12-
expect(value.factory).toBe(null);
13-
expect(value.client).toBe(null);
14-
expect(value.isReady).toBe(false);
15-
expect(value.isReadyFromCache).toBe(false);
16-
expect(value.hasTimedout).toBe(false);
17-
expect(value.isTimedout).toBe(false);
18-
expect(value.isDestroyed).toBe(false);
19-
expect(value.lastUpdate).toBe(0);
14+
expect(value).toBe(undefined);
2015
return null;
2116
}}
2217
</SplitContext.Consumer>
2318
);
2419
});
20+
21+
test('SplitContext.Consumer shows value when wrapped in a SplitFactoryProvider', () => {
22+
render(
23+
<SplitFactoryProvider >
24+
<SplitContext.Consumer>
25+
{(value) => {
26+
expect(value).toEqual(INITIAL_CONTEXT);
27+
return null;
28+
}}
29+
</SplitContext.Consumer>
30+
</SplitFactoryProvider>
31+
);
32+
});

src/__tests__/SplitFactoryProvider.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ const logSpy = jest.spyOn(console, 'log');
1414
import { ISplitFactoryProviderChildProps } from '../types';
1515
import { SplitFactoryProvider } from '../SplitFactoryProvider';
1616
import { SplitClient } from '../SplitClient';
17-
import { INITIAL_CONTEXT, SplitContext } from '../SplitContext';
17+
import { SplitContext } from '../SplitContext';
1818
import { __factories, IClientWithContext } from '../utils';
1919
import { WARN_SF_CONFIG_AND_FACTORY } from '../constants';
20+
import { INITIAL_CONTEXT } from './testUtils/utils';
2021

2122
describe('SplitFactoryProvider', () => {
2223

src/__tests__/SplitTreatments.test.tsx

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { SplitFactory } from '@splitsoftware/splitio/client';
1010
import { sdkBrowser } from './testUtils/sdkConfigs';
1111
import { getStatus, IClientWithContext } from '../utils';
1212
import { newSplitFactoryLocalhostInstance } from './testUtils/utils';
13-
import { CONTROL_WITH_CONFIG } from '../constants';
13+
import { CONTROL_WITH_CONFIG, EXCEPTION_NO_SFP } from '../constants';
1414

1515
/** Test target */
1616
import { ISplitTreatmentsChildProps, ISplitTreatmentsProps, ISplitClientProps } from '../types';
@@ -90,19 +90,15 @@ describe('SplitTreatments', () => {
9090
);
9191
});
9292

93-
// @TODO Update test in breaking change, following common practice in React libraries, like React-redux and React-query: use a falsy value as default context value, and throw an error – instead of logging it – if components are not wrapped in a SplitContext.Provider, i.e., if the context is falsy.
94-
// it('logs error and passes control treatments if rendered outside an SplitProvider component.', () => {
95-
// render(
96-
// <SplitTreatments names={featureFlagNames} >
97-
// {({ treatments }: ISplitTreatmentsChildProps) => {
98-
// expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG });
99-
// return null;
100-
// }}
101-
// </SplitTreatments>
102-
// );
103-
104-
// expect(logSpy).toBeCalledWith(WARN_ST_NO_CLIENT);
105-
// });
93+
test('throws error if invoked outside of SplitFactoryProvider.', () => {
94+
expect(() => {
95+
render(
96+
<SplitTreatments names={featureFlagNames} >
97+
{() => null}
98+
</SplitTreatments>
99+
);
100+
}).toThrow(EXCEPTION_NO_SFP);
101+
});
106102

107103
/**
108104
* Input validation. Passing invalid feature flag names or attributes while the Sdk
@@ -149,13 +145,15 @@ describe('SplitTreatments', () => {
149145

150146
test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => {
151147
render(
152-
// @ts-expect-error flagSets and names are mutually exclusive
153-
<SplitTreatments names={featureFlagNames} flagSets={flagSets} >
154-
{({ treatments }) => {
155-
expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG });
156-
return null;
157-
}}
158-
</SplitTreatments>
148+
<SplitFactoryProvider >
149+
{/* @ts-expect-error flagSets and names are mutually exclusive */}
150+
<SplitTreatments names={featureFlagNames} flagSets={flagSets} >
151+
{({ treatments }) => {
152+
expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG });
153+
return null;
154+
}}
155+
</SplitTreatments>
156+
</SplitFactoryProvider>
159157
);
160158

161159
expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.');

src/__tests__/testUtils/utils.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { render } from '@testing-library/react';
3+
import { ISplitContextValues } from '../../types';
34
const { SplitFactory: originalSplitFactory } = jest.requireActual('@splitsoftware/splitio/client');
45

56
export interface TestComponentProps {
@@ -114,3 +115,14 @@ export function testAttributesBinding(Component: React.FunctionComponent<TestCom
114115
wrapper.rerender(<Component splitKey={undefined} attributesFactory={{ at4: 'at4' }} attributesClient={undefined} testSwitch={attributesBindingSwitch} factory={factory} />);
115116
wrapper.rerender(<Component splitKey={undefined} attributesFactory={undefined} attributesClient={undefined} testSwitch={attributesBindingSwitch} factory={factory} />);
116117
}
118+
119+
export const INITIAL_CONTEXT: ISplitContextValues = {
120+
client: null,
121+
factory: null,
122+
isReady: false,
123+
isReadyFromCache: false,
124+
isTimedout: false,
125+
hasTimedout: false,
126+
lastUpdate: 0,
127+
isDestroyed: false,
128+
}

src/__tests__/useSplitClient.test.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { SplitFactoryProvider } from '../SplitFactoryProvider';
1515
import { SplitClient } from '../SplitClient';
1616
import { SplitContext } from '../SplitContext';
1717
import { testAttributesBinding, TestComponentProps } from './testUtils/utils';
18+
import { EXCEPTION_NO_SFP } from '../constants';
1819

1920
describe('useSplitClient', () => {
2021

@@ -64,18 +65,16 @@ describe('useSplitClient', () => {
6465
expect(outerFactory.client as jest.Mock).toHaveReturnedWith(client);
6566
});
6667

67-
test('returns null if invoked outside Split context.', () => {
68-
let client;
69-
let sharedClient;
70-
render(
71-
React.createElement(() => {
72-
client = useSplitClient().client;
73-
sharedClient = useSplitClient({ splitKey: 'user2', trafficType: 'user' }).client;
74-
return null;
75-
})
76-
);
77-
expect(client).toBe(null);
78-
expect(sharedClient).toBe(null);
68+
test('throws error if invoked outside of SplitFactoryProvider.', () => {
69+
expect(() => {
70+
render(
71+
React.createElement(() => {
72+
useSplitClient();
73+
useSplitClient({ splitKey: 'user2', trafficType: 'user' });
74+
return null;
75+
})
76+
);
77+
}).toThrow(EXCEPTION_NO_SFP);
7978
});
8079

8180
test('attributes binding test with utility', (done) => {
@@ -223,7 +222,7 @@ describe('useSplitClient', () => {
223222
});
224223

225224
// Remove this test once side effects are moved to the useSplitClient effect.
226-
test('must update on SDK events between the render phase (hook call) and commit phase (effect call)', () =>{
225+
test('must update on SDK events between the render phase (hook call) and commit phase (effect call)', () => {
227226
const outerFactory = SplitFactory(sdkBrowser);
228227
let count = 0;
229228

src/__tests__/useSplitManager.test.tsx

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getStatus } from '../utils';
1313
/** Test target */
1414
import { SplitFactoryProvider } from '../SplitFactoryProvider';
1515
import { useSplitManager } from '../useSplitManager';
16+
import { EXCEPTION_NO_SFP } from '../constants';
1617

1718
describe('useSplitManager', () => {
1819

@@ -55,26 +56,15 @@ describe('useSplitManager', () => {
5556
});
5657
});
5758

58-
test('returns null if invoked outside Split context.', () => {
59-
let hookResult;
60-
render(
61-
React.createElement(() => {
62-
hookResult = useSplitManager();
63-
return null;
64-
})
65-
);
66-
67-
expect(hookResult).toStrictEqual({
68-
manager: null,
69-
client: null,
70-
factory: null,
71-
hasTimedout: false,
72-
isDestroyed: false,
73-
isReady: false,
74-
isReadyFromCache: false,
75-
isTimedout: false,
76-
lastUpdate: 0,
77-
});
59+
test('throws error if invoked outside of SplitFactoryProvider.', () => {
60+
expect(() => {
61+
render(
62+
React.createElement(() => {
63+
useSplitManager();
64+
return null;
65+
})
66+
);
67+
}).toThrow(EXCEPTION_NO_SFP);
7868
});
7969

8070
});

0 commit comments

Comments
 (0)