Skip to content

Commit b8f9b4e

Browse files
beeme1mrtoddbaert
andauthored
feat: avoid re-resolving flags unaffected by a change event (#1024)
## This PR - avoid re-resolving flags unaffected by a change event ### Notes If the provider sends the change event payload, the React SDK uses the list of change flags. If the list is missing or empty, all events are triggered. This is a way to avoid sending unnecessary evaluation telemetry data. --------- Signed-off-by: Michael Beemer <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent c1374bb commit b8f9b4e

File tree

2 files changed

+93
-10
lines changed

2 files changed

+93
-10
lines changed

packages/react/src/evaluation/use-feature-flag.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
Client,
3+
ClientProviderEvents,
34
EvaluationDetails,
5+
EventHandler,
46
FlagEvaluationOptions,
57
FlagValue,
68
JsonValue,
@@ -264,6 +266,11 @@ export function useObjectFlagDetails<T extends JsonValue = JsonValue>(
264266
);
265267
}
266268

269+
// determines if a flag should be re-evaluated based on a list of changed flags
270+
function shouldEvaluateFlag(flagKey: string, flagsChanged?: string[]): boolean {
271+
return !!flagsChanged && flagsChanged.includes(flagKey);
272+
}
273+
267274
function attachHandlersAndResolve<T extends FlagValue>(
268275
flagKey: string,
269276
defaultValue: T,
@@ -309,6 +316,12 @@ function attachHandlersAndResolve<T extends FlagValue>(
309316
}
310317
};
311318

319+
const configurationChangeCallback: EventHandler<ClientProviderEvents.ConfigurationChanged> = (eventDetails) => {
320+
if (shouldEvaluateFlag(flagKey, eventDetails?.flagsChanged)) {
321+
updateEvaluationDetailsCallback();
322+
}
323+
};
324+
312325
useEffect(() => {
313326
if (status === ProviderStatus.NOT_READY) {
314327
// update when the provider is ready
@@ -329,11 +342,11 @@ function attachHandlersAndResolve<T extends FlagValue>(
329342
useEffect(() => {
330343
if (defaultedOptions.updateOnConfigurationChanged) {
331344
// update when the provider configuration changes
332-
client.addHandler(ProviderEvents.ConfigurationChanged, updateEvaluationDetailsCallback);
345+
client.addHandler(ProviderEvents.ConfigurationChanged, configurationChangeCallback);
333346
}
334347
return () => {
335348
// cleanup the handlers
336-
client.removeHandler(ProviderEvents.ConfigurationChanged, updateEvaluationDetailsCallback);
349+
client.removeHandler(ProviderEvents.ConfigurationChanged, configurationChangeCallback);
337350
};
338351
}, []);
339352

packages/react/test/evaluation.spec.tsx

+78-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1+
import '@testing-library/jest-dom'; // see: https://testing-library.com/docs/react-testing-library/setup
2+
import { act, render, renderHook, screen, waitFor } from '@testing-library/react';
3+
import * as React from 'react';
14
import {
5+
ErrorCode,
26
EvaluationContext,
7+
EvaluationDetails,
38
Hook,
49
InMemoryProvider,
510
OpenFeature,
6-
StandardResolutionReasons,
7-
EvaluationDetails,
8-
ErrorCode,
9-
} from '@openfeature/web-sdk';
10-
import '@testing-library/jest-dom'; // see: https://testing-library.com/docs/react-testing-library/setup
11-
import { act, render, renderHook, screen, waitFor } from '@testing-library/react';
12-
import * as React from 'react';
13-
import {
1411
OpenFeatureProvider,
12+
StandardResolutionReasons,
1513
useBooleanFlagDetails,
1614
useBooleanFlagValue,
1715
useFlag,
@@ -26,6 +24,7 @@ import {
2624
import { TestingProvider } from './test.utils';
2725
import { HookFlagQuery } from '../src/evaluation/hook-flag-query';
2826
import { startTransition, useState } from 'react';
27+
import { jest } from '@jest/globals';
2928

3029
describe('evaluation', () => {
3130
const EVALUATION = 'evaluation';
@@ -332,6 +331,10 @@ describe('evaluation', () => {
332331
await OpenFeature.setContext(RERENDER_DOMAIN, {});
333332
});
334333

334+
afterEach(() => {
335+
jest.clearAllMocks();
336+
});
337+
335338
it('should not rerender on context change because the evaluated values did not change', async () => {
336339
const TestComponent = TestComponentFactory();
337340
render(
@@ -366,6 +369,24 @@ describe('evaluation', () => {
366369
expect(screen.queryByTestId('render-count')).toHaveTextContent('2');
367370
});
368371

372+
it('should not render on flag change because the provider did not include changed flags in the change event', async () => {
373+
const TestComponent = TestComponentFactory();
374+
render(
375+
<OpenFeatureProvider domain={RERENDER_DOMAIN}>
376+
<TestComponent></TestComponent>
377+
</OpenFeatureProvider>,
378+
);
379+
380+
expect(screen.queryByTestId('render-count')).toHaveTextContent('1');
381+
await act(async () => {
382+
await rerenderProvider.putConfiguration({
383+
...FLAG_CONFIG,
384+
});
385+
});
386+
387+
expect(screen.queryByTestId('render-count')).toHaveTextContent('1');
388+
});
389+
369390
it('should not rerender on flag change because the evaluated values did not change', async () => {
370391
const TestComponent = TestComponentFactory();
371392
render(
@@ -393,8 +414,37 @@ describe('evaluation', () => {
393414
expect(screen.queryByTestId('render-count')).toHaveTextContent('1');
394415
});
395416

417+
it('should not rerender on flag change because the config values did not change', async () => {
418+
const TestComponent = TestComponentFactory();
419+
const resolverSpy = jest.spyOn(rerenderProvider, 'resolveBooleanEvaluation');
420+
render(
421+
<OpenFeatureProvider domain={RERENDER_DOMAIN}>
422+
<TestComponent></TestComponent>
423+
</OpenFeatureProvider>,
424+
);
425+
426+
expect(screen.queryByTestId('render-count')).toHaveTextContent('1');
427+
428+
await act(async () => {
429+
await rerenderProvider.putConfiguration({
430+
...FLAG_CONFIG,
431+
});
432+
});
433+
434+
expect(screen.queryByTestId('render-count')).toHaveTextContent('1');
435+
// The resolver should not be called again because the flag config did not change
436+
expect(resolverSpy).toHaveBeenNthCalledWith(
437+
1,
438+
BOOL_FLAG_KEY,
439+
expect.anything(),
440+
expect.anything(),
441+
expect.anything(),
442+
);
443+
});
444+
396445
it('should rerender on flag change because the evaluated values changed', async () => {
397446
const TestComponent = TestComponentFactory();
447+
const resolverSpy = jest.spyOn(rerenderProvider, 'resolveBooleanEvaluation');
398448
render(
399449
<OpenFeatureProvider domain={RERENDER_DOMAIN}>
400450
<TestComponent></TestComponent>
@@ -415,6 +465,26 @@ describe('evaluation', () => {
415465
});
416466

417467
expect(screen.queryByTestId('render-count')).toHaveTextContent('2');
468+
469+
await act(async () => {
470+
await rerenderProvider.putConfiguration({
471+
...FLAG_CONFIG,
472+
[BOOL_FLAG_KEY]: {
473+
...FLAG_CONFIG[BOOL_FLAG_KEY],
474+
// Change the default variant to trigger a rerender
475+
defaultVariant: 'on',
476+
},
477+
});
478+
});
479+
480+
expect(screen.queryByTestId('render-count')).toHaveTextContent('3');
481+
expect(resolverSpy).toHaveBeenNthCalledWith(
482+
3,
483+
BOOL_FLAG_KEY,
484+
expect.anything(),
485+
expect.anything(),
486+
expect.anything(),
487+
);
418488
});
419489
});
420490
});

0 commit comments

Comments
 (0)