Skip to content

Commit 6bcef89

Browse files
authored
feat!: disable suspense by default, add suspense hooks (#940)
This PR: - inverts the suspense defaults (we now do not suspend by default, you have to add `suspend:true` in options) - adds `useSuspenseFlag` (analogous to `useSuspenseXxx`) in other libraries, which behaves the same as `useFlag` with `{ suspend: true }` - updates README (specifically encourages use use "query-style" hooks over type-specific hooks - adds `@experimental` jsdoc marker to all suspense options and hooks - associated tests Things to consider: - I did not add `useSuspense{Type}FlagValue` and `useSuspense{Type}FlagDetails` hooks; we could do this if we wanted, but IMO these are already not the primary APIs we want to push users toward in react - we want them to use the generic `useFlag` and `useSuspenseFlag` which return the react query interfaces. Fixes: #933 --------- Signed-off-by: Todd Baert <[email protected]>
1 parent c1878e4 commit 6bcef89

File tree

5 files changed

+201
-74
lines changed

5 files changed

+201
-74
lines changed

packages/react/README.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,14 @@ You can disable this feature in the hook options (or in the [OpenFeatureProvider
199199

200200
```tsx
201201
function Page() {
202-
const showNewMessage = useBooleanFlagValue('new-message', false, { updateOnContextChanged: false });
202+
const { value: showNewMessage } = useFlag('new-message', false, { updateOnContextChanged: false });
203203
return (
204-
<MyComponents></MyComponents>
205-
)
204+
<div className="App">
205+
<header className="App-header">
206+
{showNewMessage ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
207+
</header>
208+
</div>
209+
);
206210
}
207211
```
208212

@@ -216,20 +220,29 @@ You can disable this feature in the hook options (or in the [OpenFeatureProvider
216220

217221
```tsx
218222
function Page() {
219-
const showNewMessage = useBooleanFlagValue('new-message', false, { updateOnConfigurationChanged: false });
223+
const { value: showNewMessage } = useFlag('new-message', false, { updateOnConfigurationChanged: false });
220224
return (
221-
<MyComponents></MyComponents>
222-
)
225+
<div className="App">
226+
<header className="App-header">
227+
{showNewMessage ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
228+
</header>
229+
</div>
230+
);
223231
}
224232
```
225233

226234
Note that if your provider doesn't support updates, this configuration has no impact.
227235

228236
#### Suspense Support
229237

238+
> [!NOTE]
239+
> React suspense is an experimental feature and subject to change in future versions.
240+
241+
230242
Frequently, providers need to perform some initial startup tasks.
231243
It may be desireable not to display components with feature flags until this is complete, or when the context changes.
232244
Built-in [suspense](https://react.dev/reference/react/Suspense) support makes this easy.
245+
Use `useSuspenseFlag` or pass `{ suspend: true }` in the hook options to leverage this functionality.
233246

234247
```tsx
235248
function Content() {
@@ -242,8 +255,8 @@ function Content() {
242255
}
243256

244257
function Message() {
245-
// component to render after READY.
246-
const showNewMessage = useBooleanFlagValue('new-message', false);
258+
// component to render after READY, equivalent to useFlag('new-message', false, { suspend: true });
259+
const { value: showNewMessage } = useSuspenseFlag('new-message', false);
247260

248261
return (
249262
<>
@@ -271,8 +284,9 @@ This can be disabled in the hook options (or in the [OpenFeatureProvider](#openf
271284
272285
The OpenFeature React SDK features built-in [suspense support](#suspense-support).
273286
This means that it will render your loading fallback automatically while the your provider starts up, and during context reconciliation for any of your components using feature flags!
274-
However, you will see this error if you neglect to create a suspense boundary around any components using feature flags; add a suspense boundary to resolve this issue.
275-
Alternatively, you can disable this feature by setting `suspendWhileReconciling=false` and `suspendUntilReady=false` in the [evaluation hooks](#evaluation-hooks) or the [OpenFeatureProvider](#openfeatureprovider-context-provider) (which applies to all evaluation hooks in child components).
287+
If you use suspense and neglect to create a suspense boundary around any components using feature flags, you will see this error.
288+
Add a suspense boundary to resolve this issue.
289+
Alternatively, you can disable this suspense (the default) by removing `suspendWhileReconciling=true`, `suspendUntilReady=true` or `suspend=true` in the [evaluation hooks](#evaluation-hooks) or the [OpenFeatureProvider](#openfeatureprovider-context-provider) (which applies to all evaluation hooks in child components).
276290

277291
> I get odd rendering issues, or errors when components mount, if I use the suspense features.
278292

packages/react/src/common/options.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type ReactFlagEvaluationOptions = ({
44
/**
55
* Enable or disable all suspense functionality.
66
* Cannot be used in conjunction with `suspendUntilReady` and `suspendWhileReconciling` options.
7+
* @experimental Suspense is an experimental feature subject to change in future versions.
78
*/
89
suspend?: boolean;
910
suspendUntilReady?: never;
@@ -12,15 +13,17 @@ export type ReactFlagEvaluationOptions = ({
1213
/**
1314
* Suspend flag evaluations while the provider is not ready.
1415
* Set to false if you don't want to show suspense fallbacks until the provider is initialized.
15-
* Defaults to true.
16+
* Defaults to false.
1617
* Cannot be used in conjunction with `suspend` option.
18+
* @experimental Suspense is an experimental feature subject to change in future versions.
1719
*/
1820
suspendUntilReady?: boolean;
1921
/**
2022
* Suspend flag evaluations while the provider's context is being reconciled.
2123
* Set to true if you want to show suspense fallbacks while flags are re-evaluated after context changes.
22-
* Defaults to true.
24+
* Defaults to false.
2325
* Cannot be used in conjunction with `suspend` option.
26+
* @experimental Suspense is an experimental feature subject to change in future versions.
2427
*/
2528
suspendWhileReconciling?: boolean;
2629
suspend?: never;
@@ -51,8 +54,8 @@ export type NormalizedOptions = Omit<ReactFlagEvaluationOptions, 'suspend'>;
5154
export const DEFAULT_OPTIONS: ReactFlagEvaluationOptions = {
5255
updateOnContextChanged: true,
5356
updateOnConfigurationChanged: true,
54-
suspendUntilReady: true,
55-
suspendWhileReconciling: true,
57+
suspendUntilReady: false,
58+
suspendWhileReconciling: false,
5659
};
5760

5861
/**

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ type ConstrainedFlagQuery<T> = FlagQuery<
3131
: JsonValue
3232
>;
3333

34+
// suspense options removed for the useSuspenseFlag hooks
35+
type NoSuspenseOptions = Omit<ReactFlagEvaluationOptions, 'suspend' | 'suspendUntilReady' | 'suspendWhileReconciling'>
36+
3437
/**
3538
* Evaluates a feature flag generically, returning an react-flavored queryable object.
3639
* The resolver method to use is based on the type of the defaultValue.
@@ -70,6 +73,26 @@ T extends boolean
7073
return query as unknown as ConstrainedFlagQuery<T>;
7174
}
7275

76+
// alias to the return value of useFlag, used to keep useSuspenseFlag consistent
77+
type UseFlagReturn<T extends FlagValue> = ReturnType<typeof useFlag<T>>
78+
79+
/**
80+
* Equivalent to {@link useFlag} with `options: { suspend: true }`
81+
* @experimental Suspense is an experimental feature subject to change in future versions.
82+
* @param {string} flagKey the flag identifier
83+
* @template {FlagValue} T A optional generic argument constraining the default.
84+
* @param {T} defaultValue the default value; used to determine what resolved type should be used.
85+
* @param {NoSuspenseOptions} options for this evaluation
86+
* @returns { UseFlagReturn<T> } a queryable object containing useful information about the flag.
87+
*/
88+
export function useSuspenseFlag<T extends FlagValue = FlagValue>(
89+
flagKey: string,
90+
defaultValue: T,
91+
options?: NoSuspenseOptions,
92+
): UseFlagReturn<T> {
93+
return useFlag(flagKey, defaultValue, { ...options, suspendUntilReady: true, suspendWhileReconciling: true });
94+
}
95+
7396
/**
7497
* Evaluates a feature flag, returning a boolean.
7598
* By default, components will re-render when the flag value changes.

0 commit comments

Comments
 (0)