Skip to content

Commit ec3d967

Browse files
wichopylukas-reiningtoddbaert
authored
feat: use mutate context hook (#1031)
context mutation hook --------- Signed-off-by: Will C. <[email protected]> Co-authored-by: Lukas Reining <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent 4b03c2f commit ec3d967

File tree

5 files changed

+196
-6
lines changed

5 files changed

+196
-6
lines changed

packages/react/src/provider/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { normalizeOptions } from '../common/options';
88
* DO NOT EXPORT PUBLICLY
99
* @internal
1010
*/
11-
export const Context = React.createContext<{ client: Client; options: ReactFlagEvaluationOptions } | undefined>(undefined);
11+
export const Context = React.createContext<{ client: Client; domain?: string; options: ReactFlagEvaluationOptions } | undefined>(undefined);
1212

1313
/**
1414
* Get a normalized copy of the options used for this OpenFeatureProvider, see {@link normalizeOptions}.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './provider';
22
export * from './use-open-feature-client';
33
export * from './use-when-provider-ready';
4-
export * from './test-provider';
4+
export * from './test-provider';
5+
export * from './use-context-mutator';

packages/react/src/provider/provider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,5 @@ export function OpenFeatureProvider({ client, domain, children, ...options }: Pr
3636
client = OpenFeature.getClient(domain);
3737
}
3838

39-
return <Context.Provider value={{ client, options }}>{children}</Context.Provider>;
39+
return <Context.Provider value={{ client, options, domain }}>{children}</Context.Provider>;
4040
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useCallback, useContext, useRef } from 'react';
2+
import type { EvaluationContext } from '@openfeature/web-sdk';
3+
import { OpenFeature } from '@openfeature/web-sdk';
4+
import { Context } from './context';
5+
6+
/**
7+
*
8+
* A hook for accessing context mutating functions.
9+
*
10+
*/
11+
export function useContextMutator({
12+
setGlobal
13+
}: {
14+
/**
15+
* Apply changes to the global context instead of the domain scoped context applied at the React Provider
16+
*/
17+
setGlobal?: boolean;
18+
} = {}) {
19+
const { domain } = useContext(Context) || {};
20+
const previousContext = useRef<null | EvaluationContext>(null);
21+
22+
const setContext = useCallback(async (updatedContext: EvaluationContext) => {
23+
if (previousContext.current !== updatedContext) {
24+
if (!domain || setGlobal) {
25+
OpenFeature.setContext(updatedContext);
26+
} else {
27+
OpenFeature.setContext(domain, updatedContext);
28+
}
29+
previousContext.current = updatedContext;
30+
}
31+
}, [domain]);
32+
33+
return {
34+
setContext,
35+
};
36+
}

packages/react/test/provider.spec.tsx

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import type { EvaluationContext} from '@openfeature/web-sdk';
2-
import { OpenFeature } from '@openfeature/web-sdk';
2+
import { InMemoryProvider, OpenFeature } from '@openfeature/web-sdk';
33
import '@testing-library/jest-dom'; // see: https://testing-library.com/docs/react-testing-library/setup
4-
import { render, renderHook, screen, waitFor } from '@testing-library/react';
4+
import { render, renderHook, screen, waitFor, fireEvent, act } from '@testing-library/react';
55
import * as React from 'react';
6-
import { OpenFeatureProvider, useOpenFeatureClient, useWhenProviderReady } from '../src';
6+
import {
7+
OpenFeatureProvider,
8+
useOpenFeatureClient,
9+
useWhenProviderReady,
10+
useContextMutator,
11+
useStringFlagValue,
12+
} from '../src';
713
import { TestingProvider } from './test.utils';
814

915
describe('OpenFeatureProvider', () => {
@@ -34,6 +40,9 @@ describe('OpenFeatureProvider', () => {
3440
if (context.user == '[email protected]') {
3541
return 'both';
3642
}
43+
if (context.done === true) {
44+
return 'parting';
45+
}
3746
return 'greeting';
3847
},
3948
},
@@ -138,4 +147,148 @@ describe('OpenFeatureProvider', () => {
138147
});
139148
});
140149
});
150+
describe('useMutateContext', () => {
151+
const MutateButton = () => {
152+
const { setContext } = useContextMutator();
153+
154+
return <button onClick={() => setContext({ user: '[email protected]' })}>Update Context</button>;
155+
};
156+
const TestComponent = ({ name }: { name: string }) => {
157+
const flagValue = useStringFlagValue<'hi' | 'bye' | 'aloha'>(SUSPENSE_FLAG_KEY, 'hi');
158+
159+
return (
160+
<div>
161+
<MutateButton />
162+
<div>{`${name} says ${flagValue}`}</div>
163+
</div>
164+
);
165+
};
166+
167+
it('should update context when a domain is set', async () => {
168+
const DOMAIN = 'mutate-context-tests';
169+
OpenFeature.setProvider(DOMAIN, suspendingProvider());
170+
render(
171+
<OpenFeatureProvider domain={DOMAIN}>
172+
<React.Suspense fallback={<div>{FALLBACK}</div>}>
173+
<TestComponent name="Will" />
174+
</React.Suspense>
175+
</OpenFeatureProvider>,
176+
);
177+
178+
await waitFor(() => {
179+
expect(screen.getByText('Will says hi')).toBeInTheDocument();
180+
});
181+
182+
act(() => {
183+
fireEvent.click(screen.getByText('Update Context'));
184+
});
185+
await waitFor(
186+
() => {
187+
expect(screen.getByText('Will says aloha')).toBeInTheDocument();
188+
},
189+
{ timeout: DELAY * 4 },
190+
);
191+
});
192+
193+
it('should update nested contexts', async () => {
194+
const DOMAIN1 = 'Wills Domain';
195+
const DOMAIN2 = 'Todds Domain';
196+
OpenFeature.setProvider(DOMAIN1, suspendingProvider());
197+
OpenFeature.setProvider(DOMAIN2, suspendingProvider());
198+
render(
199+
<OpenFeatureProvider domain={DOMAIN1}>
200+
<React.Suspense fallback={<div>{FALLBACK}</div>}>
201+
<TestComponent name="Will" />
202+
<OpenFeatureProvider domain={DOMAIN2}>
203+
<React.Suspense fallback={<div>{FALLBACK}</div>}>
204+
<TestComponent name="Todd" />
205+
</React.Suspense>
206+
</OpenFeatureProvider>
207+
</React.Suspense>
208+
</OpenFeatureProvider>,
209+
);
210+
211+
await waitFor(() => {
212+
expect(screen.getByText('Todd says hi')).toBeInTheDocument();
213+
});
214+
215+
act(() => {
216+
// Click the Update context button in Todds domain
217+
fireEvent.click(screen.getAllByText('Update Context')[1]);
218+
});
219+
await waitFor(
220+
() => {
221+
expect(screen.getByText('Todd says aloha')).toBeInTheDocument();
222+
},
223+
{ timeout: DELAY * 4 },
224+
);
225+
await waitFor(
226+
() => {
227+
expect(screen.getByText('Will says hi')).toBeInTheDocument();
228+
},
229+
{ timeout: DELAY * 4 },
230+
);
231+
});
232+
233+
it('should update nested global contexts', async () => {
234+
const DOMAIN1 = 'Wills Domain';
235+
OpenFeature.setProvider(DOMAIN1, suspendingProvider());
236+
OpenFeature.setProvider(new InMemoryProvider({
237+
globalFlagsHere: {
238+
defaultVariant: 'a',
239+
variants: {
240+
a: 'Smile',
241+
b: 'Frown',
242+
},
243+
disabled: false,
244+
contextEvaluator: (ctx: EvaluationContext) => {
245+
if (ctx.user === '[email protected]') {
246+
return 'b';
247+
}
248+
249+
return 'a';
250+
},
251+
}
252+
}));
253+
const GlobalComponent = ({ name }: { name: string }) => {
254+
const flagValue = useStringFlagValue<'b' | 'a'>('globalFlagsHere', 'a');
255+
256+
return (
257+
<div>
258+
<MutateButton />
259+
<div>{`${name} likes to ${flagValue}`}</div>
260+
</div>
261+
);
262+
};
263+
render(
264+
<OpenFeatureProvider domain={DOMAIN1}>
265+
<React.Suspense fallback={<div>{FALLBACK}</div>}>
266+
<TestComponent name="Will" />
267+
<OpenFeatureProvider>
268+
<React.Suspense fallback={<div>{FALLBACK}</div>}>
269+
<GlobalComponent name="Todd" />
270+
</React.Suspense>
271+
</OpenFeatureProvider>
272+
</React.Suspense>
273+
</OpenFeatureProvider>,
274+
);
275+
276+
await waitFor(() => {
277+
expect(screen.getByText('Todd likes to Smile')).toBeInTheDocument();
278+
});
279+
280+
act(() => {
281+
// Click the Update context button in Todds domain
282+
fireEvent.click(screen.getAllByText('Update Context')[1]);
283+
});
284+
await waitFor(
285+
() => {
286+
expect(screen.getByText('Todd likes to Frown')).toBeInTheDocument();
287+
},
288+
{ timeout: DELAY * 4 },
289+
);
290+
291+
expect(screen.getByText('Will says hi')).toBeInTheDocument();
292+
});
293+
});
141294
});

0 commit comments

Comments
 (0)