Skip to content
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
10 changes: 4 additions & 6 deletions packages/react/src/evaluation/use-feature-flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,9 @@ function attachHandlersAndResolve<T extends FlagValue>(
isFirstRender.current = false;
return;
}

const newDetails = resolver(client).call(client, flagKey, defaultValue, options);
if (!isEqual(newDetails.value, evaluationDetails.value)) {
if (!isEqual(newDetails, evaluationDetails)) {
setEvaluationDetails(newDetails);
}
}, [client, flagKey, defaultValue, options, resolver, evaluationDetails]);
Expand All @@ -324,11 +324,9 @@ function attachHandlersAndResolve<T extends FlagValue>(
const updatedEvaluationDetails = resolver(client).call(client, flagKey, defaultValue, options);

/**
* Avoid re-rendering if the value hasn't changed. We could expose a means
* to define a custom comparison function if users require a more
* sophisticated comparison in the future.
* Avoid re-rendering if the evaluation details haven't changed.
*/
if (!isEqual(updatedEvaluationDetails.value, evaluationDetailsRef.current.value)) {
if (!isEqual(updatedEvaluationDetails, evaluationDetailsRef.current)) {
setEvaluationDetails(updatedEvaluationDetails);
}
}, [client, flagKey, defaultValue, options, resolver]);
Expand Down
13 changes: 7 additions & 6 deletions packages/react/src/internal/is-equal.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { type FlagValue } from '@openfeature/web-sdk';

/**
* Deeply compare two values to determine if they are equal.
* Supports primitives and serializable objects.
* @param {FlagValue} value First value to compare
* @param {FlagValue} other Second value to compare
*
* Note: Does not handle Date, RegExp, Map, Set, or circular references.
* Suitable for comparing EvaluationDetails and other JSON-serializable data.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good callout.

* @param {unknown} value First value to compare
* @param {unknown} other Second value to compare
* @returns {boolean} True if the values are equal
*/
export function isEqual(value: FlagValue, other: FlagValue): boolean {
export function isEqual(value: unknown, other: unknown): boolean {
if (value === other) {
return true;
}
Expand All @@ -16,7 +17,7 @@ export function isEqual(value: FlagValue, other: FlagValue): boolean {
return false;
}

if (typeof value === 'object' && value !== null && other !== null) {
if (typeof value === 'object' && value !== null && typeof other === 'object' && other !== null) {
const valueKeys = Object.keys(value);
const otherKeys = Object.keys(other);

Expand Down
73 changes: 73 additions & 0 deletions packages/react/test/evaluation.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,79 @@ describe('evaluation', () => {
return new TestingProvider(CONFIG, DELAY); // delay init by 100ms
};

describe('provider ready event updates', () => {
it('should update EvaluationDetails when provider becomes ready (including reason)', async () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test failed before I updated the compare.

const PROVIDER_READY_DOMAIN = 'provider-ready-test';
const TEST_FLAG_KEY = 'test-flag';
const FLAG_VALUE = true;

// Create a provider that will delay initialization
const provider = new TestingProvider(
{
[TEST_FLAG_KEY]: {
disabled: false,
variants: {
on: FLAG_VALUE,
off: false,
},
defaultVariant: 'on',
},
},
DELAY,
);

OpenFeature.setProvider(PROVIDER_READY_DOMAIN, provider);

let capturedDetails: EvaluationDetails<boolean> | undefined;
let renderCount = 0;

function TestComponent() {
renderCount++;
const details = useBooleanFlagDetails(TEST_FLAG_KEY, FLAG_VALUE);
capturedDetails = details;

return (
<div>
<div data-testid="value">{String(details.value)}</div>
<div data-testid="reason">{details.reason}</div>
<div data-testid="render-count">{renderCount}</div>
</div>
);
}

render(
<OpenFeatureProvider domain={PROVIDER_READY_DOMAIN}>
<TestComponent />
</OpenFeatureProvider>,
);

// Initial render - provider is NOT_READY, should use default value with ERROR reason
expect(capturedDetails?.value).toBe(FLAG_VALUE);
expect(capturedDetails?.reason).toBe(StandardResolutionReasons.ERROR);
expect(capturedDetails?.errorCode).toBe(ErrorCode.PROVIDER_NOT_READY);
expect(screen.getByTestId('reason')).toHaveTextContent(StandardResolutionReasons.ERROR);

const initialRenderCount = renderCount;

// Wait for provider to become ready and component to re-render
await waitFor(
() => {
expect(capturedDetails?.reason).toBe(StandardResolutionReasons.STATIC);
},
{ timeout: DELAY * 2 },
);

// After provider is ready, same value but different reason and no error
expect(capturedDetails?.value).toBe(FLAG_VALUE);
expect(capturedDetails?.errorCode).toBeUndefined();
expect(screen.getByTestId('value')).toHaveTextContent(String(FLAG_VALUE));
expect(screen.getByTestId('reason')).toHaveTextContent(StandardResolutionReasons.STATIC);

// Verify that a re-render occurred
expect(renderCount).toBeGreaterThan(initialRenderCount);
});
});

describe('when using the noop provider', () => {
function TestComponent() {
const { value } = useSuspenseFlag(SUSPENSE_FLAG_KEY, DEFAULT);
Expand Down