Skip to content

Commit d1d02fa

Browse files
toddbaertbeeme1mr
andauthored
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 <[email protected]> Co-authored-by: Michael Beemer <[email protected]>
1 parent 1e13333 commit d1d02fa

File tree

2 files changed

+129
-26
lines changed

2 files changed

+129
-26
lines changed

packages/react/README.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Here's a basic example of how to use the current API with the in-memory provider
3333
```tsx
3434
import logo from './logo.svg';
3535
import './App.css';
36-
import { EvaluationContext, OpenFeatureProvider, useFeatureFlag, OpenFeature } from '@openfeature/react-sdk';
36+
import { EvaluationContext, OpenFeatureProvider, useBooleanFlagValue, useBooleanFlagDetails, OpenFeature } from '@openfeature/react-sdk';
3737
import { FlagdWebProvider } from '@openfeature/flagd-web-provider';
3838

3939
const flagConfig = {
@@ -64,12 +64,12 @@ function App() {
6464
}
6565

6666
function Page() {
67-
const booleanFlag = useFeatureFlag('new-message', false);
67+
const newMessage = useBooleanFlagValue('new-message', false);
6868
return (
6969
<div className="App">
7070
<header className="App-header">
7171
<img src={logo} className="App-logo" alt="logo" />
72-
{booleanFlag.value ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
72+
{newMessage ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
7373
</header>
7474
</div>
7575
)
@@ -78,6 +78,19 @@ function Page() {
7878
export default App;
7979
```
8080

81+
You use the detailed flag evaluation hooks to evaluate the flag and get additional information about the flag and the evaluation.
82+
83+
```tsx
84+
import { useBooleanFlagDetails} from '@openfeature/react-sdk';
85+
86+
const {
87+
value,
88+
variant,
89+
reason,
90+
flagMetadata
91+
} = useBooleanFlagDetails('new-message', false);
92+
```
93+
8194
### Multiple Providers and Scoping
8295

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

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

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

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

125138
```tsx
126139
function Page() {
127-
const booleanFlag = useFeatureFlag('new-message', false, { updateOnConfigurationChanged: false });
140+
const newMessage = useBooleanFlagValue('new-message', false, { updateOnConfigurationChanged: false });
128141
return (
129142
<MyComponents></MyComponents>
130143
)
@@ -151,11 +164,11 @@ function Content() {
151164

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

156169
return (
157170
<>
158-
{showNewMessage ? (
171+
{newMessage ? (
159172
<p>Welcome to this OpenFeature-enabled React app!</p>
160173
) : (
161174
<p>Welcome to this plain old React app!</p>

packages/react/src/use-feature-flag.ts

Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Client, EvaluationDetails, FlagEvaluationOptions, FlagValue, ProviderEvents, ProviderStatus } from '@openfeature/web-sdk';
1+
import { Client, EvaluationDetails, FlagEvaluationOptions, FlagValue, JsonValue, ProviderEvents, ProviderStatus } from '@openfeature/web-sdk';
22
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
33
import { useOpenFeatureClient } from './provider';
44

@@ -37,15 +37,117 @@ enum SuspendState {
3737
Error
3838
}
3939

40+
/**
41+
* Evaluates a feature flag, returning a boolean.
42+
* By default, components will re-render when the flag value changes.
43+
* @param {string} flagKey the flag identifier
44+
* @param {boolean} defaultValue the default value
45+
* @param {ReactFlagEvaluationOptions} options options for this evaluation
46+
* @returns { boolean} a EvaluationDetails object for this evaluation
47+
*/
48+
export function useBooleanFlagValue(flagKey: string, defaultValue: boolean, options?: ReactFlagEvaluationOptions): boolean {
49+
return useBooleanFlagDetails(flagKey, defaultValue, options).value;
50+
}
51+
4052
/**
4153
* Evaluates a feature flag, returning evaluation details.
42-
* @param {string}flagKey the flag identifier
54+
* By default, components will re-render when the flag value changes.
55+
* @param {string} flagKey the flag identifier
56+
* @param {boolean} defaultValue the default value
57+
* @param {ReactFlagEvaluationOptions} options options for this evaluation
58+
* @returns { EvaluationDetails<boolean>} a EvaluationDetails object for this evaluation
59+
*/
60+
export function useBooleanFlagDetails(flagKey: string, defaultValue: boolean, options?: ReactFlagEvaluationOptions): EvaluationDetails<boolean> {
61+
return attachHandlersAndResolve(flagKey, defaultValue, (client) => {
62+
return client.getBooleanDetails;
63+
}, options);
64+
}
65+
66+
/**
67+
* Evaluates a feature flag, returning a string.
68+
* By default, components will re-render when the flag value changes.
69+
* @param {string} flagKey the flag identifier
70+
* @template {string} [T=string] A optional generic argument constraining the string
4371
* @param {T} defaultValue the default value
4472
* @param {ReactFlagEvaluationOptions} options options for this evaluation
45-
* @template T flag type
73+
* @returns { boolean} a EvaluationDetails object for this evaluation
74+
*/
75+
export function useStringFlagValue<T extends string = string>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): T {
76+
return useStringFlagDetails(flagKey, defaultValue, options).value;
77+
}
78+
79+
/**
80+
* Evaluates a feature flag, returning evaluation details.
81+
* By default, components will re-render when the flag value changes.
82+
* @param {string} flagKey the flag identifier
83+
* @template {string} [T=string] A optional generic argument constraining the string
84+
* @param {T} defaultValue the default value
85+
* @param {ReactFlagEvaluationOptions} options options for this evaluation
86+
* @returns { EvaluationDetails<string>} a EvaluationDetails object for this evaluation
87+
*/
88+
export function useStringFlagDetails<T extends string = string>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails<T> {
89+
return attachHandlersAndResolve(flagKey, defaultValue, (client) => {
90+
return client.getStringDetails<T>;
91+
}, options);
92+
}
93+
94+
/**
95+
* Evaluates a feature flag, returning a number.
96+
* By default, components will re-render when the flag value changes.
97+
* @param {string} flagKey the flag identifier
98+
* @template {number} [T=number] A optional generic argument constraining the number
99+
* @param {T} defaultValue the default value
100+
* @param {ReactFlagEvaluationOptions} options options for this evaluation
101+
* @returns { boolean} a EvaluationDetails object for this evaluation
102+
*/
103+
export function useNumberFlagValue<T extends number = number>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): T {
104+
return useNumberFlagDetails(flagKey, defaultValue, options).value;
105+
}
106+
107+
/**
108+
* Evaluates a feature flag, returning evaluation details.
109+
* By default, components will re-render when the flag value changes.
110+
* @param {string} flagKey the flag identifier
111+
* @template {number} [T=number] A optional generic argument constraining the number
112+
* @param {T} defaultValue the default value
113+
* @param {ReactFlagEvaluationOptions} options options for this evaluation
114+
* @returns { EvaluationDetails<number>} a EvaluationDetails object for this evaluation
115+
*/
116+
export function useNumberFlagDetails<T extends number = number>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails<T> {
117+
return attachHandlersAndResolve(flagKey, defaultValue, (client) => {
118+
return client.getNumberDetails<T>;
119+
}, options);
120+
}
121+
122+
/**
123+
* Evaluates a feature flag, returning an object.
124+
* By default, components will re-render when the flag value changes.
125+
* @param {string} flagKey the flag identifier
126+
* @template {JsonValue} [T=JsonValue] A optional generic argument describing the structure
127+
* @param {T} defaultValue the default value
128+
* @param {ReactFlagEvaluationOptions} options options for this evaluation
129+
* @returns { boolean} a EvaluationDetails object for this evaluation
130+
*/
131+
export function useObjectFlagValue<T extends JsonValue = JsonValue>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): T {
132+
return useObjectFlagDetails<T>(flagKey, defaultValue, options).value;
133+
}
134+
135+
/**
136+
* Evaluates a feature flag, returning evaluation details.
137+
* By default, components will re-render when the flag value changes.
138+
* @param {string} flagKey the flag identifier
139+
* @param {T} defaultValue the default value
140+
* @template {JsonValue} [T=JsonValue] A optional generic argument describing the structure
141+
* @param {ReactFlagEvaluationOptions} options options for this evaluation
46142
* @returns { EvaluationDetails<T>} a EvaluationDetails object for this evaluation
47143
*/
48-
export function useFeatureFlag<T extends FlagValue>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails<T> {
144+
export function useObjectFlagDetails<T extends JsonValue = JsonValue>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails<T> {
145+
return attachHandlersAndResolve(flagKey, defaultValue, (client) => {
146+
return client.getObjectDetails<T>;
147+
}, options);
148+
}
149+
150+
function attachHandlersAndResolve<T extends FlagValue>(flagKey: string, defaultValue: T, resolver: (client: Client) => (flagKey: string, defaultValue: T) => EvaluationDetails<T>, options?: ReactFlagEvaluationOptions): EvaluationDetails<T> {
49151
const defaultedOptions = { ...DEFAULT_OPTIONS, ...options };
50152
const [, updateState] = useState<object | undefined>();
51153
const forceUpdate = () => {
@@ -80,19 +182,7 @@ export function useFeatureFlag<T extends FlagValue>(flagKey: string, defaultValu
80182
};
81183
}, [client]);
82184

83-
return getFlag(client, flagKey, defaultValue);
84-
}
85-
86-
function getFlag<T extends FlagValue>(client: Client, flagKey: string, defaultValue: T): EvaluationDetails<T> {
87-
if (typeof defaultValue === 'boolean') {
88-
return client.getBooleanDetails(flagKey, defaultValue) as EvaluationDetails<T>;
89-
} else if (typeof defaultValue === 'string') {
90-
return client.getStringDetails(flagKey, defaultValue) as EvaluationDetails<T>;
91-
} else if (typeof defaultValue === 'number') {
92-
return client.getNumberDetails(flagKey, defaultValue) as EvaluationDetails<T>;
93-
} else {
94-
return client.getObjectDetails(flagKey, defaultValue) as EvaluationDetails<T>;
95-
}
185+
return resolver(client).call(client, flagKey, defaultValue);
96186
}
97187

98188
/**

0 commit comments

Comments
 (0)