Skip to content

Commit

Permalink
Merge branch 'hooks_options_object' into hooks_export
Browse files Browse the repository at this point in the history
  • Loading branch information
EmilianoSanchez committed Nov 2, 2023
2 parents 5c5274b + d15e1b3 commit fe6bc13
Show file tree
Hide file tree
Showing 18 changed files with 276 additions and 244 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
- Added a new parameter `options` to `useClient` and `useTreatments` hooks, to allow controlling on what SDK events the hook should update the component. Read more in our docs.
- Added new `useSplitClient` and `useSplitTreatments` hooks. Their input parameters and logic are equivalent to `useClient` and `useTreatments` hooks, but rather than returning only the SDK client and treatments respectively, they return the Split context object together with the SDK client and treatments. Read more in our docs.
- 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).
- Updated type declarations of the library components to not restrict the type of the `children` prop to ReactElement, allowing to pass any valid ReactNode value (Related to issue https://github.com/splitio/react-client/issues/164).
- Updated the `useTreatments` hook to optimize feature flag evaluation. It now uses the `useMemo` hook to memoize calls to the SDK's `getTreatmentsWithConfig` function. This avoids re-evaluating feature flags when the hook is called with the same parameters and the feature flag definitions have not changed.
- Updated linter and other dependencies for vulnerability fixes.
- 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.
Expand Down
326 changes: 162 additions & 164 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/__tests__/SplitTreatments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ describe.each([
</SplitTreatments>
),
({ names, attributes }) => {
useSplitTreatments(names, attributes);
useSplitTreatments({ names, attributes });
renderTimes++;
return null;
}
Expand Down
12 changes: 6 additions & 6 deletions src/__tests__/useSplitClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ test('useSplitClient must update on SDK events', () => {
// Equivalent to
// - Using config key and traffic type: `const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' });`
// - Disabling update props, since the wrapping SplitFactory has them enabled: `const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });`
const { client } = useSplitClient(undefined, undefined, { att1: 'att1' });
const { client } = useSplitClient({ attributes: { att1: 'att1' } });
expect(client).toBe(mainClient); // Assert that the main client was retrieved.
expect(client!.getAttributes()).toEqual({ att1: 'att1' }); // Assert that the client was retrieved with the provided attributes.
countUseSplitClient++;
Expand All @@ -50,7 +50,7 @@ test('useSplitClient must update on SDK events', () => {
{() => { countSplitClientUser2++; return null }}
</SplitClient>
{React.createElement(() => {
const { client } = useSplitClient('user_2');
const { client } = useSplitClient({ splitKey: 'user_2' });
expect(client).toBe(user2Client);
countUseSplitClientUser2++;
return null;
Expand All @@ -59,21 +59,21 @@ test('useSplitClient must update on SDK events', () => {
{() => { countSplitClientWithUpdate++; return null }}
</SplitClient>
{React.createElement(() => {
useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, undefined, { updateOnSdkUpdate: true }).client;
useSplitClient({ splitKey: sdkBrowser.core.key, trafficType: sdkBrowser.core.trafficType, updateOnSdkUpdate: true }).client;
countUseSplitClientWithUpdate++;
return null;
})}
<SplitClient splitKey={'user_2'} updateOnSdkUpdate={true}>
{() => { countSplitClientUser2WithUpdate++; return null }}
</SplitClient>
{React.createElement(() => {
useSplitClient('user_2', undefined, undefined, { updateOnSdkUpdate: true });
useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true });
countUseSplitClientUser2WithUpdate++;
return null;
})}
<SplitClient splitKey={'user_2'} updateOnSdkUpdate={true}>
{React.createElement(() => {
const status = useSplitClient('user_2', undefined, undefined, { updateOnSdkUpdate: true });
const status = useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true });
expect(status.client).toBe(user2Client);

// useSplitClient doesn't re-render twice if it is in the context of a SplitClient with same user key and there is a SDK event
Expand Down Expand Up @@ -143,7 +143,7 @@ test('useSplitClient must support changes in update props', () => {
let rendersCount = 0;

function InnerComponent(updateOptions) {
useSplitClient(undefined, undefined, undefined, updateOptions);
useSplitClient(updateOptions);
rendersCount++;
return null;
}
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/useSplitTreatments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,21 @@ test('useSplitTreatments must update on SDK events', async () => {
{() => { countSplitTreatments++; return null }}
</SplitTreatments>
{React.createElement(() => {
const context = useSplitTreatments(['split_test'], { att1: 'att1' });
const context = useSplitTreatments({ names: ['split_test'], attributes: { att1: 'att1' } });
expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
validateTreatments(context);
countUseSplitTreatments++;
return null;
})}
{React.createElement(() => {
const context = useSplitTreatments(['split_test'], undefined, 'user_2');
const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2' });
expect(context.client).toBe(user2Client);
validateTreatments(context);
countUseSplitTreatmentsUser2++;
return null;
})}
{React.createElement(() => {
const context = useSplitTreatments(['split_test'], undefined, 'user_2', { updateOnSdkUpdate: true });
const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: true });
expect(context.client).toBe(user2Client);
validateTreatments(context);
countUseSplitTreatmentsUser2WithUpdate++;
Expand Down
24 changes: 12 additions & 12 deletions src/__tests__/useTrack.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ describe('useTrack', () => {
const value = 10;
const properties = { prop1: 'prop1' };

test('returns the track method binded to the client at Split context updated by SplitFactory.', () => {
test('returns the track method bound to the client at Split context updated by SplitFactory.', () => {
const outerFactory = SplitSdk(sdkBrowser);
let bindedTrack;
let boundTrack;
let trackResult;

render(
<SplitFactory factory={outerFactory} >
{React.createElement(() => {
bindedTrack = useTrack();
trackResult = bindedTrack(tt, eventType, value, properties);
boundTrack = useTrack();
trackResult = boundTrack(tt, eventType, value, properties);
return null;
})}
</SplitFactory>,
Expand All @@ -40,17 +40,17 @@ describe('useTrack', () => {
expect(track).toHaveReturnedWith(trackResult);
});

test('returns the track method binded to the client at Split context updated by SplitClient.', () => {
test('returns the track method bound to the client at Split context updated by SplitClient.', () => {
const outerFactory = SplitSdk(sdkBrowser);
let bindedTrack;
let boundTrack;
let trackResult;

render(
<SplitFactory factory={outerFactory} >
<SplitClient splitKey='user2' >
{React.createElement(() => {
bindedTrack = useTrack();
trackResult = bindedTrack(tt, eventType, value, properties);
boundTrack = useTrack();
trackResult = boundTrack(tt, eventType, value, properties);
return null;
})}
</SplitClient>
Expand All @@ -61,16 +61,16 @@ describe('useTrack', () => {
expect(track).toHaveReturnedWith(trackResult);
});

test('returns the track method binded to a new client given a splitKey and optional trafficType.', () => {
test('returns the track method bound to a new client given a splitKey and optional trafficType.', () => {
const outerFactory = SplitSdk(sdkBrowser);
let bindedTrack;
let boundTrack;
let trackResult;

render(
<SplitFactory factory={outerFactory} >
{React.createElement(() => {
bindedTrack = useTrack('user2', tt);
trackResult = bindedTrack(eventType, value, properties);
boundTrack = useTrack('user2', tt);
trackResult = boundTrack(eventType, value, properties);
return null;
})}
</SplitFactory>,
Expand Down
48 changes: 34 additions & 14 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SplitIO from '@splitsoftware/splitio/types/splitio';
import type { ReactNode } from 'react';

/**
* Split Status interface. It represents the current readiness state of the SDK.
Expand Down Expand Up @@ -124,26 +125,19 @@ export interface ISplitFactoryProps extends IUpdateProps {
/**
* Children of the SplitFactory component. It can be a functional component (child as a function) or a React element.
*/
children: ((props: ISplitFactoryChildProps) => JSX.Element | null) | JSX.Element | null;
children: ((props: ISplitFactoryChildProps) => ReactNode) | ReactNode;
}

/**
* SplitClient Child Props interface. These are the props that the child component receives from the 'SplitClient' component.
* useSplitClient options interface. This is the options object accepted by useSplitClient hook,
* used to retrieve a client instance with the Split context, and listen for SDK events.
*/
// @TODO remove next type (breaking-change)
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ISplitClientChildProps extends ISplitContextValues { }

/**
* SplitClient Props interface. These are the props accepted by SplitClient component,
* used to instantiate a new client instance, update the Split context, and listen for SDK events.
*/
export interface ISplitClientProps extends IUpdateProps {
export interface IUseSplitClientOptions extends IUpdateProps {

/**
* The customer identifier.
*/
splitKey: SplitIO.SplitKey;
splitKey?: SplitIO.SplitKey;

/**
* Traffic type associated with the customer identifier.
Expand All @@ -155,11 +149,37 @@ export interface ISplitClientProps extends IUpdateProps {
* An object of type Attributes used to evaluate the feature flags.
*/
attributes?: SplitIO.Attributes;
}

/**
* SplitClient Child Props interface. These are the props that the child component receives from the 'SplitClient' component.
*/
// @TODO remove next type (breaking-change)
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ISplitClientChildProps extends ISplitContextValues { }

/**
* SplitClient Props interface. These are the props accepted by SplitClient component,
* used to instantiate a new client instance, update the Split context, and listen for SDK events.
*/
export interface ISplitClientProps extends IUseSplitClientOptions {

/**
* Children of the SplitFactory component. It can be a functional component (child as a function) or a React element.
*/
children: ((props: ISplitClientChildProps) => JSX.Element | null) | JSX.Element | null;
children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode;
}

/**
* useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook,
* used to call 'client.getTreatmentsWithConfig()' and retrieve the result together with the Split context.
*/
export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions {

/**
* list of feature flag names
*/
names: string[]
}

/**
Expand Down Expand Up @@ -197,5 +217,5 @@ export interface ISplitTreatmentsProps {
/**
* Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show.
*/
children: ((props: ISplitTreatmentsChildProps) => JSX.Element | null);
children: ((props: ISplitTreatmentsChildProps) => ReactNode);
}
5 changes: 2 additions & 3 deletions src/useClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { IUpdateProps } from './types';
import { useSplitClient } from './useSplitClient';

/**
Expand All @@ -9,6 +8,6 @@ import { useSplitClient } from './useSplitClient';
* @return A Split Client instance, or null if used outside the scope of SplitFactory
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients}
*/
export function useClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options?: IUpdateProps): SplitIO.IBrowserClient | null {
return useSplitClient(key, trafficType, attributes, options).client;
export function useClient(splitKey?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null {
return useSplitClient({ splitKey, trafficType, attributes }).client;
}
10 changes: 5 additions & 5 deletions src/useSplitClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { SplitContext } from './SplitContext';
import { getSplitClient, initAttributes, IClientWithContext, getStatus } from './utils';
import { ISplitContextValues, IUpdateProps } from './types';
import { ISplitContextValues, IUseSplitClientOptions } from './types';

export const DEFAULT_UPDATE_OPTIONS = {
updateOnSdkUpdate: false,
Expand All @@ -17,17 +17,17 @@ export const DEFAULT_UPDATE_OPTIONS = {
* @return A Split Context object
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients}
*/
export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes, options?: IUpdateProps): ISplitContextValues {
export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextValues {
const {
updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate
updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate, splitKey, trafficType, attributes
} = { ...DEFAULT_UPDATE_OPTIONS, ...options };

const context = React.useContext(SplitContext);
const { client: contextClient, factory } = context;

let client = contextClient as IClientWithContext;
if (key && factory) {
client = getSplitClient(factory, key, trafficType);
if (splitKey && factory) {
client = getSplitClient(factory, splitKey, trafficType);
}
initAttributes(client, attributes);

Expand Down
13 changes: 7 additions & 6 deletions src/useSplitTreatments.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { getControlTreatmentsWithConfig } from './constants';
import { IClientWithContext, memoizeGetTreatmentsWithConfig } from './utils';
import { ISplitTreatmentsChildProps, IUpdateProps } from './types';
import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types';
import { useSplitClient } from './useSplitClient';

/**
Expand All @@ -11,15 +11,16 @@ import { useSplitClient } from './useSplitClient';
* @return A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if split names do not exist.
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations}
*/
export function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): ISplitTreatmentsChildProps {
const context = useSplitClient(key, undefined, undefined, options);
const client = context.client;
export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps {
const context = useSplitClient({...options, attributes: undefined });
const { client, lastUpdate } = context;
const { names, attributes } = options;

const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []);

const treatments = client && (client as IClientWithContext).__getStatus().isOperational ?
getTreatmentsWithConfig(client, context.lastUpdate, splitNames, attributes, { ...client.getAttributes() }) :
getControlTreatmentsWithConfig(splitNames);
getTreatmentsWithConfig(client, lastUpdate, names, attributes, { ...client.getAttributes() }) :
getControlTreatmentsWithConfig(names);

return {
...context,
Expand Down
8 changes: 4 additions & 4 deletions src/useTrack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useClient } from './useClient';
import { useSplitClient } from './useSplitClient';

// no-op function that returns false
const noOpFalse = () => false;
Expand All @@ -7,11 +7,11 @@ const noOpFalse = () => false;
* 'useTrack' is a hook that returns the track method from a Split client.
* It uses the 'useContext' hook to access the client from the Split context.
*
* @return A track function binded to a Split client. If the client is not available, the result is a no-op function that returns false.
* @return A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false.
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track}
*/
export function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] {
export function useTrack(splitKey?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] {
// All update options are false to avoid re-renders. The track method doesn't need the client to be operational.
const client = useClient(key, trafficType, undefined, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });
const { client } = useSplitClient({ splitKey, trafficType, updateOnSdkReady: false, updateOnSdkReadyFromCache: false });
return client ? client.track.bind(client) : noOpFalse;
}
5 changes: 2 additions & 3 deletions src/useTreatments.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { IUpdateProps } from './types';
import { useSplitTreatments } from './useSplitTreatments';

/**
Expand All @@ -9,6 +8,6 @@ import { useSplitTreatments } from './useSplitTreatments';
* @return A TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist.
* @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations}
*/
export function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey, options?: IUpdateProps): SplitIO.TreatmentsWithConfig {
return useSplitTreatments(featureFlagNames, attributes, key, options).treatments;
export function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, splitKey?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig {
return useSplitTreatments({ names: featureFlagNames, attributes, splitKey }).treatments;
}
Loading

0 comments on commit fe6bc13

Please sign in to comment.