From 01288f7f7e2b4577be5f14cb99a105926e2a9431 Mon Sep 17 00:00:00 2001 From: MattIPv4 Date: Wed, 26 Nov 2025 00:45:54 +0000 Subject: [PATCH 1/3] Allow method to be passed into setContext hook Signed-off-by: MattIPv4 --- .../react/src/context/use-context-mutator.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/react/src/context/use-context-mutator.ts b/packages/react/src/context/use-context-mutator.ts index 800688baf..b3f69ab21 100644 --- a/packages/react/src/context/use-context-mutator.ts +++ b/packages/react/src/context/use-context-mutator.ts @@ -21,7 +21,7 @@ export type ContextMutation = { * @param updatedContext * @returns Promise for awaiting the context update */ - setContext: (updatedContext: EvaluationContext) => Promise; + setContext: (updatedContext: EvaluationContext | ((previousContext: EvaluationContext) => EvaluationContext)) => Promise; }; /** @@ -34,16 +34,20 @@ export function useContextMutator(options: ContextMutationOptions = { defaultCon const { domain } = useContext(Context) || {}; const previousContext = useRef(null); - const setContext = useCallback(async (updatedContext: EvaluationContext) => { - if (previousContext.current !== updatedContext) { + const setContext = useCallback(async (updatedContext: EvaluationContext | ((previousContext: EvaluationContext) => EvaluationContext)): Promise => { + const resolvedContext = typeof updatedContext === 'function' + ? updatedContext(OpenFeature.getContext(options?.defaultContext ? undefined : domain)) + : updatedContext; + + if (previousContext.current !== resolvedContext) { if (!domain || options?.defaultContext) { - OpenFeature.setContext(updatedContext); + OpenFeature.setContext(resolvedContext); } else { - OpenFeature.setContext(domain, updatedContext); + OpenFeature.setContext(domain, resolvedContext); } - previousContext.current = updatedContext; + previousContext.current = resolvedContext; } - }, [domain]); + }, [domain, options?.defaultContext]); return { setContext, From 9de51c7e918dbfc54033d3e1e43ed7cf829c6353 Mon Sep 17 00:00:00 2001 From: MattIPv4 Date: Wed, 26 Nov 2025 00:55:24 +0000 Subject: [PATCH 2/3] Add new test for setContext hook method Signed-off-by: MattIPv4 --- packages/react/test/provider.spec.tsx | 41 ++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/react/test/provider.spec.tsx b/packages/react/test/provider.spec.tsx index 91277140e..b5f0731b3 100644 --- a/packages/react/test/provider.spec.tsx +++ b/packages/react/test/provider.spec.tsx @@ -162,17 +162,18 @@ describe('OpenFeatureProvider', () => { }); }); describe('useMutateContext', () => { - const MutateButton = () => { + const MutateButton = ({ setter }: { setter?: (prevContext: EvaluationContext) => EvaluationContext }) => { const { setContext } = useContextMutator(); - return ; + return ; }; - const TestComponent = ({ name }: { name: string }) => { + + const TestComponent = ({ name, setter }: { name: string; setter?: (prevContext: EvaluationContext) => EvaluationContext }) => { const flagValue = useStringFlagValue<'hi' | 'bye' | 'aloha'>(SUSPENSE_FLAG_KEY, 'hi'); return (
- +
{`${name} says ${flagValue}`}
); @@ -304,5 +305,37 @@ describe('OpenFeatureProvider', () => { expect(screen.getByText('Will says aloha')).toBeInTheDocument(); }); + + it('should accept a method taking the previous context', async () => { + const DOMAIN = 'mutate-context-with-function'; + OpenFeature.setProvider(DOMAIN, suspendingProvider(), { done: false }); + + const setter = jest.fn((prevContext: EvaluationContext) => ({ ...prevContext, user: 'bob@flags.com' })); + render( + + {FALLBACK}}> + + + , + ); + + 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 }, + ); + + expect(setter).toHaveBeenCalledTimes(1); + expect(setter).toHaveBeenCalledWith({ done: false }); + expect(OpenFeature.getContext(DOMAIN)).toEqual({ done: false, user: 'bob@flags.com' }); + }); }); }); From d3745c39e6c022871b788fd9de9a72bb380eee1c Mon Sep 17 00:00:00 2001 From: MattIPv4 Date: Wed, 26 Nov 2025 18:18:12 +0000 Subject: [PATCH 3/3] Update JSDoc to reference accepting a method --- packages/react/src/context/use-context-mutator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/context/use-context-mutator.ts b/packages/react/src/context/use-context-mutator.ts index b3f69ab21..8ab11ac47 100644 --- a/packages/react/src/context/use-context-mutator.ts +++ b/packages/react/src/context/use-context-mutator.ts @@ -18,10 +18,10 @@ export type ContextMutation = { * Context-aware function to set the desired context (see: {@link ContextMutationOptions} for details). * There's generally no need to await the result of this function; flag evaluation hooks will re-render when the context is updated. * This promise never rejects. - * @param updatedContext + * @param updatedContext New context object or method to generate it from the current context * @returns Promise for awaiting the context update */ - setContext: (updatedContext: EvaluationContext | ((previousContext: EvaluationContext) => EvaluationContext)) => Promise; + setContext: (updatedContext: EvaluationContext | ((currentContext: EvaluationContext) => EvaluationContext)) => Promise; }; /** @@ -34,7 +34,7 @@ export function useContextMutator(options: ContextMutationOptions = { defaultCon const { domain } = useContext(Context) || {}; const previousContext = useRef(null); - const setContext = useCallback(async (updatedContext: EvaluationContext | ((previousContext: EvaluationContext) => EvaluationContext)): Promise => { + const setContext = useCallback(async (updatedContext: EvaluationContext | ((currentContext: EvaluationContext) => EvaluationContext)): Promise => { const resolvedContext = typeof updatedContext === 'function' ? updatedContext(OpenFeature.getContext(options?.defaultContext ? undefined : domain)) : updatedContext;