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!: remove generic hook, add specific type hooks #766

Merged
merged 6 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>;
toddbaert marked this conversation as resolved.
Show resolved Hide resolved
}
return resolver(client).call(client, flagKey, defaultValue);
}

/**
Expand Down