From 286ddb745d3b68b7b7302553d15ecbdfa320bf3d Mon Sep 17 00:00:00 2001 From: Will Chou Date: Sun, 6 Oct 2024 14:15:30 -0400 Subject: [PATCH 01/11] feat: use mutate context hook Signed-off-by: Will Chou --- packages/react/src/provider/index.ts | 3 ++- packages/react/src/provider/use-context-mutator.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/provider/use-context-mutator.ts diff --git a/packages/react/src/provider/index.ts b/packages/react/src/provider/index.ts index 8e12f357b..a4ee0392b 100644 --- a/packages/react/src/provider/index.ts +++ b/packages/react/src/provider/index.ts @@ -1,4 +1,5 @@ export * from './provider'; export * from './use-open-feature-client'; export * from './use-when-provider-ready'; -export * from './test-provider'; \ No newline at end of file +export * from './test-provider'; +export * from './use-context-mutator'; \ No newline at end of file diff --git a/packages/react/src/provider/use-context-mutator.ts b/packages/react/src/provider/use-context-mutator.ts new file mode 100644 index 000000000..3f2da533c --- /dev/null +++ b/packages/react/src/provider/use-context-mutator.ts @@ -0,0 +1,9 @@ +import { OpenFeature, EvaluationContext } from '@openfeature/web-sdk'; + +export function useContextMutator() { + return { + mutateContext: async (domain: string, updatedContext: EvaluationContext) => { + await OpenFeature.setContext(domain, updatedContext); + } + }; +} \ No newline at end of file From 83836bcc73b3b4562a95f3bd3cf458ba3daf4886 Mon Sep 17 00:00:00 2001 From: Will Chou Date: Sun, 6 Oct 2024 14:21:36 -0400 Subject: [PATCH 02/11] js doc Signed-off-by: Will Chou --- .../react/src/provider/use-context-mutator.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/react/src/provider/use-context-mutator.ts b/packages/react/src/provider/use-context-mutator.ts index 3f2da533c..d022abbbf 100644 --- a/packages/react/src/provider/use-context-mutator.ts +++ b/packages/react/src/provider/use-context-mutator.ts @@ -1,7 +1,27 @@ import { OpenFeature, EvaluationContext } from '@openfeature/web-sdk'; -export function useContextMutator() { +type DomainContextMutator = (domain: string, updatedContext: EvaluationContext) => Promise; + +type ContextMutatorReturn = { + mutateContext: DomainContextMutator; +} +/** + * + * A hook for accessing context mutating functions. + * + * @returns {ContextMutatorReturn} + */ +export function useContextMutator(): { + mutateContext: DomainContextMutator; +} { return { + /** + * + * Mutates the evaluation context for a given domain. + * + * @param {string}domain + * @param {EvaluationContext} updatedContext + */ mutateContext: async (domain: string, updatedContext: EvaluationContext) => { await OpenFeature.setContext(domain, updatedContext); } From b360c2336df0ddd7a7f295d4cb42b261746d6a04 Mon Sep 17 00:00:00 2001 From: Will Chou Date: Sun, 6 Oct 2024 14:34:47 -0400 Subject: [PATCH 03/11] handle domain and global Signed-off-by: Will Chou --- .../react/src/provider/use-context-mutator.ts | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/react/src/provider/use-context-mutator.ts b/packages/react/src/provider/use-context-mutator.ts index d022abbbf..535063e0f 100644 --- a/packages/react/src/provider/use-context-mutator.ts +++ b/packages/react/src/provider/use-context-mutator.ts @@ -2,28 +2,22 @@ import { OpenFeature, EvaluationContext } from '@openfeature/web-sdk'; type DomainContextMutator = (domain: string, updatedContext: EvaluationContext) => Promise; -type ContextMutatorReturn = { - mutateContext: DomainContextMutator; -} /** * * A hook for accessing context mutating functions. * - * @returns {ContextMutatorReturn} */ -export function useContextMutator(): { - mutateContext: DomainContextMutator; -} { - return { - /** - * - * Mutates the evaluation context for a given domain. - * - * @param {string}domain - * @param {EvaluationContext} updatedContext - */ - mutateContext: async (domain: string, updatedContext: EvaluationContext) => { - await OpenFeature.setContext(domain, updatedContext); +export function useContextMutator() { + + async function mutateContext(domainOrUpdatedContext: string | EvaluationContext, updatedContextOrUndefined?: EvaluationContext): Promise { + if (typeof domainOrUpdatedContext === 'string' && updatedContextOrUndefined) { + await OpenFeature.setContext(domainOrUpdatedContext, updatedContextOrUndefined); + } else if (typeof domainOrUpdatedContext !== 'string') { + OpenFeature.setContext(domainOrUpdatedContext); } + } + + return { + mutateContext, }; } \ No newline at end of file From 9c10f55bd07677920afd464178037986eaac443c Mon Sep 17 00:00:00 2001 From: Will Chou Date: Mon, 7 Oct 2024 14:56:29 -0400 Subject: [PATCH 04/11] commets, reuse domain from context value Signed-off-by: Will Chou --- packages/react/src/provider/context.ts | 2 +- .../react/src/provider/use-context-mutator.ts | 25 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/react/src/provider/context.ts b/packages/react/src/provider/context.ts index ff4f215d3..d04ca3f69 100644 --- a/packages/react/src/provider/context.ts +++ b/packages/react/src/provider/context.ts @@ -7,7 +7,7 @@ import { NormalizedOptions, ReactFlagEvaluationOptions, normalizeOptions } from * 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}. diff --git a/packages/react/src/provider/use-context-mutator.ts b/packages/react/src/provider/use-context-mutator.ts index 535063e0f..ac12eba42 100644 --- a/packages/react/src/provider/use-context-mutator.ts +++ b/packages/react/src/provider/use-context-mutator.ts @@ -1,23 +1,24 @@ +import { useContext } from 'react'; import { OpenFeature, EvaluationContext } from '@openfeature/web-sdk'; - -type DomainContextMutator = (domain: string, updatedContext: EvaluationContext) => Promise; +import { Context } from './context'; /** - * + * * A hook for accessing context mutating functions. - * + * */ export function useContextMutator() { + async function mutateContext(updatedContext: EvaluationContext): Promise { + const { domain } = useContext(Context) || {}; - async function mutateContext(domainOrUpdatedContext: string | EvaluationContext, updatedContextOrUndefined?: EvaluationContext): Promise { - if (typeof domainOrUpdatedContext === 'string' && updatedContextOrUndefined) { - await OpenFeature.setContext(domainOrUpdatedContext, updatedContextOrUndefined); - } else if (typeof domainOrUpdatedContext !== 'string') { - OpenFeature.setContext(domainOrUpdatedContext); + if (!domain) { + throw new Error('No domain set for your context'); } + + OpenFeature.setContext(domain, updatedContext); } - + return { - mutateContext, + mutateContext, }; -} \ No newline at end of file +} From c8991f577c00626d261c35a531e62897efea0b6e Mon Sep 17 00:00:00 2001 From: Will Chou Date: Mon, 7 Oct 2024 15:17:04 -0400 Subject: [PATCH 05/11] set in provider Signed-off-by: Will Chou --- packages/react/src/provider/provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/provider/provider.tsx b/packages/react/src/provider/provider.tsx index e3fb928a3..4f9600adb 100644 --- a/packages/react/src/provider/provider.tsx +++ b/packages/react/src/provider/provider.tsx @@ -35,5 +35,5 @@ export function OpenFeatureProvider({ client, domain, children, ...options }: Pr client = OpenFeature.getClient(domain); } - return {children}; + return {children}; } From f438017ff65dc62a27442324dd90b9b09688c827 Mon Sep 17 00:00:00 2001 From: Will Chou Date: Tue, 8 Oct 2024 06:00:24 -0400 Subject: [PATCH 06/11] test: the hook Signed-off-by: Will Chou --- .../react/src/provider/use-context-mutator.ts | 8 +- packages/react/test/provider.spec.tsx | 76 ++++++++++++++++++- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/packages/react/src/provider/use-context-mutator.ts b/packages/react/src/provider/use-context-mutator.ts index ac12eba42..e28c0957d 100644 --- a/packages/react/src/provider/use-context-mutator.ts +++ b/packages/react/src/provider/use-context-mutator.ts @@ -8,11 +8,13 @@ import { Context } from './context'; * */ export function useContextMutator() { - async function mutateContext(updatedContext: EvaluationContext): Promise { - const { domain } = useContext(Context) || {}; + const { domain } = useContext(Context) || {}; + async function mutateContext(updatedContext: EvaluationContext): Promise { if (!domain) { - throw new Error('No domain set for your context'); + // Set the global context + OpenFeature.setContext(updatedContext); + return; } OpenFeature.setContext(domain, updatedContext); diff --git a/packages/react/test/provider.spec.tsx b/packages/react/test/provider.spec.tsx index fca7f8ad5..f94d67f55 100644 --- a/packages/react/test/provider.spec.tsx +++ b/packages/react/test/provider.spec.tsx @@ -1,8 +1,8 @@ import { EvaluationContext, 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', () => { @@ -33,6 +33,9 @@ describe('OpenFeatureProvider', () => { if (context.user == 'bob@flags.com') { return 'both'; } + if (context.done === true) { + return 'parting'; + } return 'greeting'; }, }, @@ -136,5 +139,74 @@ describe('OpenFeatureProvider', () => { await waitFor(() => expect(screen.queryByText('👍')).toBeInTheDocument(), { timeout: DELAY * 2 }); }); }); + + describe('useMutateContext', () => { + const MutateButton = () => { + const { mutateContext } = useContextMutator(); + + return ; + }; + const TestComponent = ({ name }: { name: string}) => { + const flagValue = useStringFlagValue<'hi' | 'bye' | 'aloha'>(SUSPENSE_FLAG_KEY, 'hi'); + + return
+ +
{`${name} says ${flagValue}`}
+
; + }; + + it('should update context when a domain is set', async () => { + const DOMAIN = 'mutate-context-tests'; + OpenFeature.setProvider(DOMAIN, suspendingProvider()); + 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 }); + }); + + it('should update nested contexts', async () => { + const DOMAIN1 = 'Wills Domain'; + const DOMAIN2 = 'Todds Domain'; + OpenFeature.setProvider(DOMAIN1, suspendingProvider()); + OpenFeature.setProvider(DOMAIN2, suspendingProvider()); + render( + {FALLBACK}}> + + + {FALLBACK}}> + + + + + ,); + + 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 }); + }); + }); }); }); From ba9081f077dd13f720b2ea69e314bf139e16cfad Mon Sep 17 00:00:00 2001 From: "Will C." Date: Sat, 12 Oct 2024 15:21:58 -0400 Subject: [PATCH 07/11] memoize the function and dont run if the object reference does not change Co-authored-by: Lukas Reining Signed-off-by: Will C. --- .../react/src/provider/use-context-mutator.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/react/src/provider/use-context-mutator.ts b/packages/react/src/provider/use-context-mutator.ts index e28c0957d..8742f9d8f 100644 --- a/packages/react/src/provider/use-context-mutator.ts +++ b/packages/react/src/provider/use-context-mutator.ts @@ -8,19 +8,21 @@ import { Context } from './context'; * */ export function useContextMutator() { - const { domain } = useContext(Context) || {}; + const { domain } = useContext(Context) || {}; + const previousContext = useRef(null); - async function mutateContext(updatedContext: EvaluationContext): Promise { - if (!domain) { - // Set the global context - OpenFeature.setContext(updatedContext); - return; - } + const mutateContext = useCallback(async (updatedContext: EvaluationContext) => { + if (previousContext.current !== updatedContext) { + if (!domain) { + OpenFeature.setContext(updatedContext); + } else { + OpenFeature.setContext(domain, updatedContext); + } + previousContext.current = updatedContext; + } + }, [domain]); - OpenFeature.setContext(domain, updatedContext); - } - - return { - mutateContext, - }; + return { + mutateContext, + }; } From 2bab15a2e4269dbddf317d892491b96e40f3e46d Mon Sep 17 00:00:00 2001 From: Will Chou Date: Sat, 12 Oct 2024 21:47:32 -0400 Subject: [PATCH 08/11] fix commit Signed-off-by: Will Chou --- packages/react/src/provider/use-context-mutator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/provider/use-context-mutator.ts b/packages/react/src/provider/use-context-mutator.ts index 8742f9d8f..4aa2138ef 100644 --- a/packages/react/src/provider/use-context-mutator.ts +++ b/packages/react/src/provider/use-context-mutator.ts @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import { useCallback, useContext, useRef } from 'react'; import { OpenFeature, EvaluationContext } from '@openfeature/web-sdk'; import { Context } from './context'; @@ -9,7 +9,7 @@ import { Context } from './context'; */ export function useContextMutator() { const { domain } = useContext(Context) || {}; - const previousContext = useRef(null); + const previousContext = useRef(null); const mutateContext = useCallback(async (updatedContext: EvaluationContext) => { if (previousContext.current !== updatedContext) { From 097e1c341417ed594814996cd0231fa896ae774b Mon Sep 17 00:00:00 2001 From: Will Chou Date: Tue, 15 Oct 2024 22:58:31 -0400 Subject: [PATCH 09/11] apply PR comments Signed-off-by: Will Chou --- .../react/src/provider/use-context-mutator.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/react/src/provider/use-context-mutator.ts b/packages/react/src/provider/use-context-mutator.ts index 4aa2138ef..bd82a9ba8 100644 --- a/packages/react/src/provider/use-context-mutator.ts +++ b/packages/react/src/provider/use-context-mutator.ts @@ -7,13 +7,20 @@ import { Context } from './context'; * A hook for accessing context mutating functions. * */ -export function useContextMutator() { +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); - const mutateContext = useCallback(async (updatedContext: EvaluationContext) => { + const setContext = useCallback(async (updatedContext: EvaluationContext) => { if (previousContext.current !== updatedContext) { - if (!domain) { + if (!domain || setGlobal) { OpenFeature.setContext(updatedContext); } else { OpenFeature.setContext(domain, updatedContext); @@ -23,6 +30,6 @@ export function useContextMutator() { }, [domain]); return { - mutateContext, + setContext, }; } From ed35f05dd4ab458578bf3988707184c08ad2968f Mon Sep 17 00:00:00 2001 From: Will Chou Date: Tue, 15 Oct 2024 23:15:46 -0400 Subject: [PATCH 10/11] test globally nested context Signed-off-by: Will Chou --- packages/react/test/provider.spec.tsx | 177 +++++++++++++++++++------- 1 file changed, 129 insertions(+), 48 deletions(-) diff --git a/packages/react/test/provider.spec.tsx b/packages/react/test/provider.spec.tsx index f94d67f55..46762f02a 100644 --- a/packages/react/test/provider.spec.tsx +++ b/packages/react/test/provider.spec.tsx @@ -1,8 +1,14 @@ -import { EvaluationContext, OpenFeature } from '@openfeature/web-sdk'; +import { EvaluationContext, 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, fireEvent, act } from '@testing-library/react'; import * as React from 'react'; -import { OpenFeatureProvider, useOpenFeatureClient, useWhenProviderReady, useContextMutator, useStringFlagValue } from '../src'; +import { + OpenFeatureProvider, + useOpenFeatureClient, + useWhenProviderReady, + useContextMutator, + useStringFlagValue, +} from '../src'; import { TestingProvider } from './test.utils'; describe('OpenFeatureProvider', () => { @@ -139,74 +145,149 @@ describe('OpenFeatureProvider', () => { await waitFor(() => expect(screen.queryByText('👍')).toBeInTheDocument(), { timeout: DELAY * 2 }); }); }); + }); + describe('useMutateContext', () => { + const MutateButton = () => { + const { setContext } = useContextMutator(); - describe('useMutateContext', () => { - const MutateButton = () => { - const { mutateContext } = useContextMutator(); - - return ; - }; - const TestComponent = ({ name }: { name: string}) => { - const flagValue = useStringFlagValue<'hi' | 'bye' | 'aloha'>(SUSPENSE_FLAG_KEY, 'hi'); + return ; + }; + const TestComponent = ({ name }: { name: string }) => { + const flagValue = useStringFlagValue<'hi' | 'bye' | 'aloha'>(SUSPENSE_FLAG_KEY, 'hi'); - return
+ return ( +
{`${name} says ${flagValue}`}
-
; - }; +
+ ); + }; - it('should update context when a domain is set', async () => { - const DOMAIN = 'mutate-context-tests'; - OpenFeature.setProvider(DOMAIN, suspendingProvider()); - render( + it('should update context when a domain is set', async () => { + const DOMAIN = 'mutate-context-tests'; + OpenFeature.setProvider(DOMAIN, suspendingProvider()); + render( + {FALLBACK}}> - + - ,); + , + ); - await waitFor(() => { - expect(screen.getByText('Will says hi')).toBeInTheDocument(); - }); + 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 }); + 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( + it('should update nested contexts', async () => { + const DOMAIN1 = 'Wills Domain'; + const DOMAIN2 = 'Todds Domain'; + OpenFeature.setProvider(DOMAIN1, suspendingProvider()); + OpenFeature.setProvider(DOMAIN2, suspendingProvider()); + render( + {FALLBACK}}> - + {FALLBACK}}> - + - ,); + , + ); - await waitFor(() => { - expect(screen.getByText('Todd says hi')).toBeInTheDocument(); - }); + 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(() => { + 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(() => { + }, + { timeout: DELAY * 4 }, + ); + await waitFor( + () => { expect(screen.getByText('Will says hi')).toBeInTheDocument(); - }, { timeout: DELAY * 4 }); + }, + { 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 === 'bob@flags.com') { + return 'b'; + } + + return 'a'; + }, + } + })); + const GlobalComponent = ({ name }: { name: string }) => { + const flagValue = useStringFlagValue<'b' | 'a'>('globalFlagsHere', 'a'); + + return ( +
+ +
{`${name} likes to ${flagValue}`}
+
+ ); + }; + render( + + {FALLBACK}}> + + + {FALLBACK}}> + + + + + , + ); + + 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(); }); }); }); From 57b5cb675b41fc7d34788a0a779412b2335749c0 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 17 Oct 2024 12:07:44 -0400 Subject: [PATCH 11/11] Update use-context-mutator.ts Signed-off-by: Todd Baert --- packages/react/src/provider/use-context-mutator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react/src/provider/use-context-mutator.ts b/packages/react/src/provider/use-context-mutator.ts index bd82a9ba8..86dd242d3 100644 --- a/packages/react/src/provider/use-context-mutator.ts +++ b/packages/react/src/provider/use-context-mutator.ts @@ -1,5 +1,6 @@ import { useCallback, useContext, useRef } from 'react'; -import { OpenFeature, EvaluationContext } from '@openfeature/web-sdk'; +import type { EvaluationContext } from '@openfeature/web-sdk'; +import { OpenFeature } from '@openfeature/web-sdk'; import { Context } from './context'; /**