From d1d02fa59de5b5b1b8866c0b5d3de1a5bc0c5a04 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 18 Jan 2024 11:44:43 -0500 Subject: [PATCH] feat!: remove generic hook, add specific type hooks (#766) 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 Co-authored-by: Michael Beemer --- packages/react/README.md | 31 +++++-- packages/react/src/use-feature-flag.ts | 124 +++++++++++++++++++++---- 2 files changed, 129 insertions(+), 26 deletions(-) diff --git a/packages/react/README.md b/packages/react/README.md index 0a6475d04..69cad84f1 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -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 = { @@ -64,12 +64,12 @@ function App() { } function Page() { - const booleanFlag = useFeatureFlag('new-message', false); + const newMessage = useBooleanFlagValue('new-message', false); return (
logo - {booleanFlag.value ?

Welcome to this OpenFeature-enabled React app!

:

Welcome to this React app.

} + {newMessage ?

Welcome to this OpenFeature-enabled React app!

:

Welcome to this React app.

}
) @@ -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`: @@ -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 ( ) @@ -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 ( ) @@ -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 ? (

Welcome to this OpenFeature-enabled React app!

) : (

Welcome to this plain old React app!

diff --git a/packages/react/src/use-feature-flag.ts b/packages/react/src/use-feature-flag.ts index 0f95f50bf..353aa6349 100644 --- a/packages/react/src/use-feature-flag.ts +++ b/packages/react/src/use-feature-flag.ts @@ -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'; @@ -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} a EvaluationDetails object for this evaluation + */ +export function useBooleanFlagDetails(flagKey: string, defaultValue: boolean, options?: ReactFlagEvaluationOptions): EvaluationDetails { + 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(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} a EvaluationDetails object for this evaluation + */ +export function useStringFlagDetails(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails { + return attachHandlersAndResolve(flagKey, defaultValue, (client) => { + return client.getStringDetails; + }, 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(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} a EvaluationDetails object for this evaluation + */ +export function useNumberFlagDetails(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails { + return attachHandlersAndResolve(flagKey, defaultValue, (client) => { + return client.getNumberDetails; + }, 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(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): T { + return useObjectFlagDetails(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} a EvaluationDetails object for this evaluation */ -export function useFeatureFlag(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails { +export function useObjectFlagDetails(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails { + return attachHandlersAndResolve(flagKey, defaultValue, (client) => { + return client.getObjectDetails; + }, options); +} + +function attachHandlersAndResolve(flagKey: string, defaultValue: T, resolver: (client: Client) => (flagKey: string, defaultValue: T) => EvaluationDetails, options?: ReactFlagEvaluationOptions): EvaluationDetails { const defaultedOptions = { ...DEFAULT_OPTIONS, ...options }; const [, updateState] = useState(); const forceUpdate = () => { @@ -80,19 +182,7 @@ export function useFeatureFlag(flagKey: string, defaultValu }; }, [client]); - return getFlag(client, flagKey, defaultValue); -} - -function getFlag(client: Client, flagKey: string, defaultValue: T): EvaluationDetails { - if (typeof defaultValue === 'boolean') { - return client.getBooleanDetails(flagKey, defaultValue) as EvaluationDetails; - } else if (typeof defaultValue === 'string') { - return client.getStringDetails(flagKey, defaultValue) as EvaluationDetails; - } else if (typeof defaultValue === 'number') { - return client.getNumberDetails(flagKey, defaultValue) as EvaluationDetails; - } else { - return client.getObjectDetails(flagKey, defaultValue) as EvaluationDetails; - } + return resolver(client).call(client, flagKey, defaultValue); } /**