Skip to content
2 changes: 1 addition & 1 deletion packages/react/src/provider/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { normalizeOptions } from '../common/options';
* DO NOT EXPORT PUBLICLY
* @internal
*/
export const Context = React.createContext<{ client: Client; options: ReactFlagEvaluationOptions } | undefined>(undefined);
export const Context = React.createContext<{ client: Client; domain?: string; options: ReactFlagEvaluationOptions } | undefined>(undefined);

/**
* Get a normalized copy of the options used for this OpenFeatureProvider, see {@link normalizeOptions}.
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/provider/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './provider';
export * from './use-open-feature-client';
export * from './use-when-provider-ready';
export * from './test-provider';
export * from './test-provider';
export * from './use-context-mutator';
2 changes: 1 addition & 1 deletion packages/react/src/provider/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ export function OpenFeatureProvider({ client, domain, children, ...options }: Pr
client = OpenFeature.getClient(domain);
}

return <Context.Provider value={{ client, options }}>{children}</Context.Provider>;
return <Context.Provider value={{ client, options, domain }}>{children}</Context.Provider>;
}
36 changes: 36 additions & 0 deletions packages/react/src/provider/use-context-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useCallback, useContext, useRef } from 'react';
import type { EvaluationContext } from '@openfeature/web-sdk';
import { OpenFeature } from '@openfeature/web-sdk';
import { Context } from './context';

/**

Check warning on line 6 in packages/react/src/provider/use-context-mutator.ts

View workflow job for this annotation

GitHub Actions / build-test-lint (18.x)

Missing JSDoc @param "root0" declaration

Check warning on line 6 in packages/react/src/provider/use-context-mutator.ts

View workflow job for this annotation

GitHub Actions / build-test-lint (18.x)

Missing JSDoc @param "root0.setGlobal" declaration

Check warning on line 6 in packages/react/src/provider/use-context-mutator.ts

View workflow job for this annotation

GitHub Actions / build-test-lint (18.x)

Missing JSDoc @returns declaration

Check warning on line 6 in packages/react/src/provider/use-context-mutator.ts

View workflow job for this annotation

GitHub Actions / build-test-lint (20.x)

Missing JSDoc @param "root0" declaration

Check warning on line 6 in packages/react/src/provider/use-context-mutator.ts

View workflow job for this annotation

GitHub Actions / build-test-lint (20.x)

Missing JSDoc @param "root0.setGlobal" declaration

Check warning on line 6 in packages/react/src/provider/use-context-mutator.ts

View workflow job for this annotation

GitHub Actions / build-test-lint (20.x)

Missing JSDoc @returns declaration
*
* A hook for accessing context mutating functions.
*
*/
export function useContextMutator({
setGlobal
}: {
/**
* Apply changes to the global context instead of the domain scoped context applied at the React Provider
*/
setGlobal?: boolean;
} = {}) {
const { domain } = useContext(Context) || {};
const previousContext = useRef<null | EvaluationContext>(null);

const setContext = useCallback(async (updatedContext: EvaluationContext) => {
if (previousContext.current !== updatedContext) {
if (!domain || setGlobal) {
OpenFeature.setContext(updatedContext);
} else {
OpenFeature.setContext(domain, updatedContext);
}
previousContext.current = updatedContext;
}
}, [domain]);

return {
setContext,
};
}
159 changes: 156 additions & 3 deletions packages/react/test/provider.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { EvaluationContext} from '@openfeature/web-sdk';
import { OpenFeature } from '@openfeature/web-sdk';
import { InMemoryProvider, OpenFeature } from '@openfeature/web-sdk';
import '@testing-library/jest-dom'; // see: https://testing-library.com/docs/react-testing-library/setup
import { render, renderHook, screen, waitFor } from '@testing-library/react';
import { render, renderHook, screen, waitFor, fireEvent, act } from '@testing-library/react';
import * as React from 'react';
import { OpenFeatureProvider, useOpenFeatureClient, useWhenProviderReady } from '../src';
import {
OpenFeatureProvider,
useOpenFeatureClient,
useWhenProviderReady,
useContextMutator,
useStringFlagValue,
} from '../src';
import { TestingProvider } from './test.utils';

describe('OpenFeatureProvider', () => {
Expand Down Expand Up @@ -34,6 +40,9 @@ describe('OpenFeatureProvider', () => {
if (context.user == '[email protected]') {
return 'both';
}
if (context.done === true) {
return 'parting';
}
return 'greeting';
},
},
Expand Down Expand Up @@ -138,4 +147,148 @@ describe('OpenFeatureProvider', () => {
});
});
});
describe('useMutateContext', () => {
const MutateButton = () => {
const { setContext } = useContextMutator();

return <button onClick={() => setContext({ user: '[email protected]' })}>Update Context</button>;
};
const TestComponent = ({ name }: { name: string }) => {
const flagValue = useStringFlagValue<'hi' | 'bye' | 'aloha'>(SUSPENSE_FLAG_KEY, 'hi');

return (
<div>
<MutateButton />
<div>{`${name} says ${flagValue}`}</div>
</div>
);
};

it('should update context when a domain is set', async () => {
const DOMAIN = 'mutate-context-tests';
OpenFeature.setProvider(DOMAIN, suspendingProvider());
render(
<OpenFeatureProvider domain={DOMAIN}>
<React.Suspense fallback={<div>{FALLBACK}</div>}>
<TestComponent name="Will" />
</React.Suspense>
</OpenFeatureProvider>,
);

await waitFor(() => {
expect(screen.getByText('Will says hi')).toBeInTheDocument();
});

act(() => {
fireEvent.click(screen.getByText('Update Context'));
});
await waitFor(
() => {
expect(screen.getByText('Will says aloha')).toBeInTheDocument();
},
{ timeout: DELAY * 4 },
);
});

it('should update nested contexts', async () => {
const DOMAIN1 = 'Wills Domain';
const DOMAIN2 = 'Todds Domain';
OpenFeature.setProvider(DOMAIN1, suspendingProvider());
OpenFeature.setProvider(DOMAIN2, suspendingProvider());
render(
<OpenFeatureProvider domain={DOMAIN1}>
<React.Suspense fallback={<div>{FALLBACK}</div>}>
<TestComponent name="Will" />
<OpenFeatureProvider domain={DOMAIN2}>
<React.Suspense fallback={<div>{FALLBACK}</div>}>
<TestComponent name="Todd" />
</React.Suspense>
</OpenFeatureProvider>
</React.Suspense>
</OpenFeatureProvider>,
);

await waitFor(() => {
expect(screen.getByText('Todd says hi')).toBeInTheDocument();
});

act(() => {
// Click the Update context button in Todds domain
fireEvent.click(screen.getAllByText('Update Context')[1]);
});
await waitFor(
() => {
expect(screen.getByText('Todd says aloha')).toBeInTheDocument();
},
{ timeout: DELAY * 4 },
);
await waitFor(
() => {
expect(screen.getByText('Will says hi')).toBeInTheDocument();
},
{ timeout: DELAY * 4 },
);
});

it('should update nested global contexts', async () => {
const DOMAIN1 = 'Wills Domain';
OpenFeature.setProvider(DOMAIN1, suspendingProvider());
OpenFeature.setProvider(new InMemoryProvider({
globalFlagsHere: {
defaultVariant: 'a',
variants: {
a: 'Smile',
b: 'Frown',
},
disabled: false,
contextEvaluator: (ctx: EvaluationContext) => {
if (ctx.user === '[email protected]') {
return 'b';
}

return 'a';
},
}
}));
const GlobalComponent = ({ name }: { name: string }) => {
const flagValue = useStringFlagValue<'b' | 'a'>('globalFlagsHere', 'a');

return (
<div>
<MutateButton />
<div>{`${name} likes to ${flagValue}`}</div>
</div>
);
};
render(
<OpenFeatureProvider domain={DOMAIN1}>
<React.Suspense fallback={<div>{FALLBACK}</div>}>
<TestComponent name="Will" />
<OpenFeatureProvider>
<React.Suspense fallback={<div>{FALLBACK}</div>}>
<GlobalComponent name="Todd" />
</React.Suspense>
</OpenFeatureProvider>
</React.Suspense>
</OpenFeatureProvider>,
);

await waitFor(() => {
expect(screen.getByText('Todd likes to Smile')).toBeInTheDocument();
});

act(() => {
// Click the Update context button in Todds domain
fireEvent.click(screen.getAllByText('Update Context')[1]);
});
await waitFor(
() => {
expect(screen.getByText('Todd likes to Frown')).toBeInTheDocument();
},
{ timeout: DELAY * 4 },
);

expect(screen.getByText('Will says hi')).toBeInTheDocument();
});
});
});
Loading