Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use mutate context hook #1031

Merged
merged 13 commits into from
Oct 17, 2024
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