Skip to content

Commit

Permalink
feat!: remove generic hook, add specific type hooks (#766)
Browse files Browse the repository at this point in the history
This PR brings the react SDK's evaluation API in-line with other SDKS.
It does this by:

- adding flag value hooks for each type (these all use common code, with
only differing generic args)
- adding flag details hooks for each type (again using common code)
- adding optional generic constraints for each

I think this is important before a non-experimental release for 2
reasons:
- it's consistent with our other JS components and other SDKs
- it fixes a potential bug if uses accidentally pass the wrong default
type

:warning: This is a breaking change.

---------

Signed-off-by: Todd Baert <[email protected]>
Co-authored-by: Michael Beemer <[email protected]>
  • Loading branch information
toddbaert and beeme1mr authored Jan 18, 2024
1 parent 1e13333 commit d1d02fa
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 26 deletions.
31 changes: 22 additions & 9 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Here's a basic example of how to use the current API with the in-memory provider
```tsx
import logo from './logo.svg';
import './App.css';
import { EvaluationContext, OpenFeatureProvider, useFeatureFlag, OpenFeature } from '@openfeature/react-sdk';
import { EvaluationContext, OpenFeatureProvider, useBooleanFlagValue, useBooleanFlagDetails, OpenFeature } from '@openfeature/react-sdk';
import { FlagdWebProvider } from '@openfeature/flagd-web-provider';

const flagConfig = {
Expand Down Expand Up @@ -64,12 +64,12 @@ function App() {
}

function Page() {
const booleanFlag = useFeatureFlag('new-message', false);
const newMessage = useBooleanFlagValue('new-message', false);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
{booleanFlag.value ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
{newMessage ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
</header>
</div>
)
Expand All @@ -78,6 +78,19 @@ function Page() {
export default App;
```

You use the detailed flag evaluation hooks to evaluate the flag and get additional information about the flag and the evaluation.

```tsx
import { useBooleanFlagDetails} from '@openfeature/react-sdk';

const {
value,
variant,
reason,
flagMetadata
} = useBooleanFlagDetails('new-message', false);
```

### Multiple Providers and Scoping

Multiple providers and scoped clients can be configured by passing a `clientName` to the `OpenFeatureProvider`:
Expand All @@ -103,11 +116,11 @@ OpenFeature.getClient('myClient');

By default, if the OpenFeature [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) is modified, components will be re-rendered.
This is useful in cases where flag values are dependant on user-attributes or other application state (user logged in, items in card, etc).
You can disable this feature in the `useFeatureFlag` hook options:
You can disable this feature in the hook options:

```tsx
function Page() {
const booleanFlag = useFeatureFlag('new-message', false, { updateOnContextChanged: false });
const newMessage = useBooleanFlagValue('new-message', false, { updateOnContextChanged: false });
return (
<MyComponents></MyComponents>
)
Expand All @@ -120,11 +133,11 @@ For more information about how evaluation context works in the React SDK, see th

By default, if the underlying provider emits a `ConfigurationChanged` event, components will be re-rendered.
This is useful if you want your UI to immediately reflect changes in the backend flag configuration.
You can disable this feature in the `useFeatureFlag` hook options:
You can disable this feature in the hook options:

```tsx
function Page() {
const booleanFlag = useFeatureFlag('new-message', false, { updateOnConfigurationChanged: false });
const newMessage = useBooleanFlagValue('new-message', false, { updateOnConfigurationChanged: false });
return (
<MyComponents></MyComponents>
)
Expand All @@ -151,11 +164,11 @@ function Content() {

function Message() {
// component to render after READY.
const { value: showNewMessage } = useFeatureFlag('new-message', false);
const newMessage = useBooleanFlagValue('new-message', false);

return (
<>
{showNewMessage ? (
{newMessage ? (
<p>Welcome to this OpenFeature-enabled React app!</p>
) : (
<p>Welcome to this plain old React app!</p>
Expand Down
124 changes: 107 additions & 17 deletions packages/react/src/use-feature-flag.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Client, EvaluationDetails, FlagEvaluationOptions, FlagValue, ProviderEvents, ProviderStatus } from '@openfeature/web-sdk';
import { Client, EvaluationDetails, FlagEvaluationOptions, FlagValue, JsonValue, ProviderEvents, ProviderStatus } from '@openfeature/web-sdk';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useOpenFeatureClient } from './provider';

Expand Down Expand Up @@ -37,15 +37,117 @@ enum SuspendState {
Error
}

/**
* Evaluates a feature flag, returning a boolean.
* By default, components will re-render when the flag value changes.
* @param {string} flagKey the flag identifier
* @param {boolean} defaultValue the default value
* @param {ReactFlagEvaluationOptions} options options for this evaluation
* @returns { boolean} a EvaluationDetails object for this evaluation
*/
export function useBooleanFlagValue(flagKey: string, defaultValue: boolean, options?: ReactFlagEvaluationOptions): boolean {
return useBooleanFlagDetails(flagKey, defaultValue, options).value;
}

/**
* Evaluates a feature flag, returning evaluation details.
* @param {string}flagKey the flag identifier
* By default, components will re-render when the flag value changes.
* @param {string} flagKey the flag identifier
* @param {boolean} defaultValue the default value
* @param {ReactFlagEvaluationOptions} options options for this evaluation
* @returns { EvaluationDetails<boolean>} a EvaluationDetails object for this evaluation
*/
export function useBooleanFlagDetails(flagKey: string, defaultValue: boolean, options?: ReactFlagEvaluationOptions): EvaluationDetails<boolean> {
return attachHandlersAndResolve(flagKey, defaultValue, (client) => {
return client.getBooleanDetails;
}, options);
}

/**
* Evaluates a feature flag, returning a string.
* By default, components will re-render when the flag value changes.
* @param {string} flagKey the flag identifier
* @template {string} [T=string] A optional generic argument constraining the string
* @param {T} defaultValue the default value
* @param {ReactFlagEvaluationOptions} options options for this evaluation
* @template T flag type
* @returns { boolean} a EvaluationDetails object for this evaluation
*/
export function useStringFlagValue<T extends string = string>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): T {
return useStringFlagDetails(flagKey, defaultValue, options).value;
}

/**
* Evaluates a feature flag, returning evaluation details.
* By default, components will re-render when the flag value changes.
* @param {string} flagKey the flag identifier
* @template {string} [T=string] A optional generic argument constraining the string
* @param {T} defaultValue the default value
* @param {ReactFlagEvaluationOptions} options options for this evaluation
* @returns { EvaluationDetails<string>} a EvaluationDetails object for this evaluation
*/
export function useStringFlagDetails<T extends string = string>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails<T> {
return attachHandlersAndResolve(flagKey, defaultValue, (client) => {
return client.getStringDetails<T>;
}, options);
}

/**
* Evaluates a feature flag, returning a number.
* By default, components will re-render when the flag value changes.
* @param {string} flagKey the flag identifier
* @template {number} [T=number] A optional generic argument constraining the number
* @param {T} defaultValue the default value
* @param {ReactFlagEvaluationOptions} options options for this evaluation
* @returns { boolean} a EvaluationDetails object for this evaluation
*/
export function useNumberFlagValue<T extends number = number>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): T {
return useNumberFlagDetails(flagKey, defaultValue, options).value;
}

/**
* Evaluates a feature flag, returning evaluation details.
* By default, components will re-render when the flag value changes.
* @param {string} flagKey the flag identifier
* @template {number} [T=number] A optional generic argument constraining the number
* @param {T} defaultValue the default value
* @param {ReactFlagEvaluationOptions} options options for this evaluation
* @returns { EvaluationDetails<number>} a EvaluationDetails object for this evaluation
*/
export function useNumberFlagDetails<T extends number = number>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails<T> {
return attachHandlersAndResolve(flagKey, defaultValue, (client) => {
return client.getNumberDetails<T>;
}, options);
}

/**
* Evaluates a feature flag, returning an object.
* By default, components will re-render when the flag value changes.
* @param {string} flagKey the flag identifier
* @template {JsonValue} [T=JsonValue] A optional generic argument describing the structure
* @param {T} defaultValue the default value
* @param {ReactFlagEvaluationOptions} options options for this evaluation
* @returns { boolean} a EvaluationDetails object for this evaluation
*/
export function useObjectFlagValue<T extends JsonValue = JsonValue>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): T {
return useObjectFlagDetails<T>(flagKey, defaultValue, options).value;
}

/**
* Evaluates a feature flag, returning evaluation details.
* By default, components will re-render when the flag value changes.
* @param {string} flagKey the flag identifier
* @param {T} defaultValue the default value
* @template {JsonValue} [T=JsonValue] A optional generic argument describing the structure
* @param {ReactFlagEvaluationOptions} options options for this evaluation
* @returns { EvaluationDetails<T>} a EvaluationDetails object for this evaluation
*/
export function useFeatureFlag<T extends FlagValue>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails<T> {
export function useObjectFlagDetails<T extends JsonValue = JsonValue>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails<T> {
return attachHandlersAndResolve(flagKey, defaultValue, (client) => {
return client.getObjectDetails<T>;
}, options);
}

function attachHandlersAndResolve<T extends FlagValue>(flagKey: string, defaultValue: T, resolver: (client: Client) => (flagKey: string, defaultValue: T) => EvaluationDetails<T>, options?: ReactFlagEvaluationOptions): EvaluationDetails<T> {
const defaultedOptions = { ...DEFAULT_OPTIONS, ...options };
const [, updateState] = useState<object | undefined>();
const forceUpdate = () => {
Expand Down Expand Up @@ -80,19 +182,7 @@ export function useFeatureFlag<T extends FlagValue>(flagKey: string, defaultValu
};
}, [client]);

return getFlag(client, flagKey, defaultValue);
}

function getFlag<T extends FlagValue>(client: Client, flagKey: string, defaultValue: T): EvaluationDetails<T> {
if (typeof defaultValue === 'boolean') {
return client.getBooleanDetails(flagKey, defaultValue) as EvaluationDetails<T>;
} else if (typeof defaultValue === 'string') {
return client.getStringDetails(flagKey, defaultValue) as EvaluationDetails<T>;
} else if (typeof defaultValue === 'number') {
return client.getNumberDetails(flagKey, defaultValue) as EvaluationDetails<T>;
} else {
return client.getObjectDetails(flagKey, defaultValue) as EvaluationDetails<T>;
}
return resolver(client).call(client, flagKey, defaultValue);
}

/**
Expand Down

0 comments on commit d1d02fa

Please sign in to comment.