Skip to content

Commit 279f9bf

Browse files
authored
feat: allow method to be passed into setContext hook (#1301)
## This PR Allows for `setContext` returned by the `useContextMutator` hook to be passed a method that returns the new context, with the current (previous) context passed into it. This aligns the signature of `setContext` to the setter of a regular `useState` hook, making it easier to conditionally update the context without needing to keep track of the domain separately from the React context. --------- Signed-off-by: MattIPv4 <[email protected]>
1 parent 8133a4f commit 279f9bf

File tree

2 files changed

+49
-12
lines changed

2 files changed

+49
-12
lines changed

packages/react/src/context/use-context-mutator.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ export type ContextMutation = {
1818
* Context-aware function to set the desired context (see: {@link ContextMutationOptions} for details).
1919
* There's generally no need to await the result of this function; flag evaluation hooks will re-render when the context is updated.
2020
* This promise never rejects.
21-
* @param updatedContext
21+
* @param updatedContext New context object or method to generate it from the current context
2222
* @returns Promise for awaiting the context update
2323
*/
24-
setContext: (updatedContext: EvaluationContext) => Promise<void>;
24+
setContext: (updatedContext: EvaluationContext | ((currentContext: EvaluationContext) => EvaluationContext)) => Promise<void>;
2525
};
2626

2727
/**
@@ -34,16 +34,20 @@ export function useContextMutator(options: ContextMutationOptions = { defaultCon
3434
const { domain } = useContext(Context) || {};
3535
const previousContext = useRef<null | EvaluationContext>(null);
3636

37-
const setContext = useCallback(async (updatedContext: EvaluationContext) => {
38-
if (previousContext.current !== updatedContext) {
37+
const setContext = useCallback(async (updatedContext: EvaluationContext | ((currentContext: EvaluationContext) => EvaluationContext)): Promise<void> => {
38+
const resolvedContext = typeof updatedContext === 'function'
39+
? updatedContext(OpenFeature.getContext(options?.defaultContext ? undefined : domain))
40+
: updatedContext;
41+
42+
if (previousContext.current !== resolvedContext) {
3943
if (!domain || options?.defaultContext) {
40-
OpenFeature.setContext(updatedContext);
44+
OpenFeature.setContext(resolvedContext);
4145
} else {
42-
OpenFeature.setContext(domain, updatedContext);
46+
OpenFeature.setContext(domain, resolvedContext);
4347
}
44-
previousContext.current = updatedContext;
48+
previousContext.current = resolvedContext;
4549
}
46-
}, [domain]);
50+
}, [domain, options?.defaultContext]);
4751

4852
return {
4953
setContext,

packages/react/test/provider.spec.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,17 +162,18 @@ describe('OpenFeatureProvider', () => {
162162
});
163163
});
164164
describe('useMutateContext', () => {
165-
const MutateButton = () => {
165+
const MutateButton = ({ setter }: { setter?: (prevContext: EvaluationContext) => EvaluationContext }) => {
166166
const { setContext } = useContextMutator();
167167

168-
return <button onClick={() => setContext({ user: '[email protected]' })}>Update Context</button>;
168+
return <button onClick={() => setContext(setter ?? { user: '[email protected]' })}>Update Context</button>;
169169
};
170-
const TestComponent = ({ name }: { name: string }) => {
170+
171+
const TestComponent = ({ name, setter }: { name: string; setter?: (prevContext: EvaluationContext) => EvaluationContext }) => {
171172
const flagValue = useStringFlagValue<'hi' | 'bye' | 'aloha'>(SUSPENSE_FLAG_KEY, 'hi');
172173

173174
return (
174175
<div>
175-
<MutateButton />
176+
<MutateButton setter={setter} />
176177
<div>{`${name} says ${flagValue}`}</div>
177178
</div>
178179
);
@@ -304,5 +305,37 @@ describe('OpenFeatureProvider', () => {
304305

305306
expect(screen.getByText('Will says aloha')).toBeInTheDocument();
306307
});
308+
309+
it('should accept a method taking the previous context', async () => {
310+
const DOMAIN = 'mutate-context-with-function';
311+
OpenFeature.setProvider(DOMAIN, suspendingProvider(), { done: false });
312+
313+
const setter = jest.fn((prevContext: EvaluationContext) => ({ ...prevContext, user: '[email protected]' }));
314+
render(
315+
<OpenFeatureProvider domain={DOMAIN}>
316+
<React.Suspense fallback={<div>{FALLBACK}</div>}>
317+
<TestComponent name="Will" setter={setter} />
318+
</React.Suspense>
319+
</OpenFeatureProvider>,
320+
);
321+
322+
await waitFor(() => {
323+
expect(screen.getByText('Will says hi')).toBeInTheDocument();
324+
});
325+
326+
act(() => {
327+
fireEvent.click(screen.getByText('Update Context'));
328+
});
329+
await waitFor(
330+
() => {
331+
expect(screen.getByText('Will says aloha')).toBeInTheDocument();
332+
},
333+
{ timeout: DELAY * 4 },
334+
);
335+
336+
expect(setter).toHaveBeenCalledTimes(1);
337+
expect(setter).toHaveBeenCalledWith({ done: false });
338+
expect(OpenFeature.getContext(DOMAIN)).toEqual({ done: false, user: '[email protected]' });
339+
});
307340
});
308341
});

0 commit comments

Comments
 (0)