Skip to content

Commit

Permalink
feat!: disable suspense by default, add suspense hooks (#940)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
toddbaert authored May 13, 2024
1 parent c1878e4 commit 6bcef89
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 74 deletions.
34 changes: 24 additions & 10 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,14 @@ You can disable this feature in the hook options (or in the [OpenFeatureProvider

```tsx
function Page() {
const showNewMessage = useBooleanFlagValue('new-message', false, { updateOnContextChanged: false });
const { value: showNewMessage } = useFlag('new-message', false, { updateOnContextChanged: false });
return (
<MyComponents></MyComponents>
)
<div className="App">
<header className="App-header">
{showNewMessage ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
</header>
</div>
);
}
```

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

```tsx
function Page() {
const showNewMessage = useBooleanFlagValue('new-message', false, { updateOnConfigurationChanged: false });
const { value: showNewMessage } = useFlag('new-message', false, { updateOnConfigurationChanged: false });
return (
<MyComponents></MyComponents>
)
<div className="App">
<header className="App-header">
{showNewMessage ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
</header>
</div>
);
}
```

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

#### Suspense Support

> [!NOTE]
> React suspense is an experimental feature and subject to change in future versions.

Frequently, providers need to perform some initial startup tasks.
It may be desireable not to display components with feature flags until this is complete, or when the context changes.
Built-in [suspense](https://react.dev/reference/react/Suspense) support makes this easy.
Use `useSuspenseFlag` or pass `{ suspend: true }` in the hook options to leverage this functionality.

```tsx
function Content() {
Expand All @@ -242,8 +255,8 @@ function Content() {
}

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

return (
<>
Expand Down Expand Up @@ -271,8 +284,9 @@ This can be disabled in the hook options (or in the [OpenFeatureProvider](#openf
The OpenFeature React SDK features built-in [suspense support](#suspense-support).
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!
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.
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).
If you use suspense and neglect to create a suspense boundary around any components using feature flags, you will see this error.
Add a suspense boundary to resolve this issue.
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).

> I get odd rendering issues, or errors when components mount, if I use the suspense features.
Expand Down
11 changes: 7 additions & 4 deletions packages/react/src/common/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type ReactFlagEvaluationOptions = ({
/**
* Enable or disable all suspense functionality.
* Cannot be used in conjunction with `suspendUntilReady` and `suspendWhileReconciling` options.
* @experimental Suspense is an experimental feature subject to change in future versions.
*/
suspend?: boolean;
suspendUntilReady?: never;
Expand All @@ -12,15 +13,17 @@ export type ReactFlagEvaluationOptions = ({
/**
* Suspend flag evaluations while the provider is not ready.
* Set to false if you don't want to show suspense fallbacks until the provider is initialized.
* Defaults to true.
* Defaults to false.
* Cannot be used in conjunction with `suspend` option.
* @experimental Suspense is an experimental feature subject to change in future versions.
*/
suspendUntilReady?: boolean;
/**
* Suspend flag evaluations while the provider's context is being reconciled.
* Set to true if you want to show suspense fallbacks while flags are re-evaluated after context changes.
* Defaults to true.
* Defaults to false.
* Cannot be used in conjunction with `suspend` option.
* @experimental Suspense is an experimental feature subject to change in future versions.
*/
suspendWhileReconciling?: boolean;
suspend?: never;
Expand Down Expand Up @@ -51,8 +54,8 @@ export type NormalizedOptions = Omit<ReactFlagEvaluationOptions, 'suspend'>;
export const DEFAULT_OPTIONS: ReactFlagEvaluationOptions = {
updateOnContextChanged: true,
updateOnConfigurationChanged: true,
suspendUntilReady: true,
suspendWhileReconciling: true,
suspendUntilReady: false,
suspendWhileReconciling: false,
};

/**
Expand Down
23 changes: 23 additions & 0 deletions packages/react/src/evaluation/use-feature-flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type ConstrainedFlagQuery<T> = FlagQuery<
: JsonValue
>;

// suspense options removed for the useSuspenseFlag hooks
type NoSuspenseOptions = Omit<ReactFlagEvaluationOptions, 'suspend' | 'suspendUntilReady' | 'suspendWhileReconciling'>

/**
* Evaluates a feature flag generically, returning an react-flavored queryable object.
* The resolver method to use is based on the type of the defaultValue.
Expand Down Expand Up @@ -70,6 +73,26 @@ T extends boolean
return query as unknown as ConstrainedFlagQuery<T>;
}

// alias to the return value of useFlag, used to keep useSuspenseFlag consistent
type UseFlagReturn<T extends FlagValue> = ReturnType<typeof useFlag<T>>

/**
* Equivalent to {@link useFlag} with `options: { suspend: true }`
* @experimental Suspense is an experimental feature subject to change in future versions.
* @param {string} flagKey the flag identifier
* @template {FlagValue} T A optional generic argument constraining the default.
* @param {T} defaultValue the default value; used to determine what resolved type should be used.
* @param {NoSuspenseOptions} options for this evaluation
* @returns { UseFlagReturn<T> } a queryable object containing useful information about the flag.
*/
export function useSuspenseFlag<T extends FlagValue = FlagValue>(
flagKey: string,
defaultValue: T,
options?: NoSuspenseOptions,
): UseFlagReturn<T> {
return useFlag(flagKey, defaultValue, { ...options, suspendUntilReady: true, suspendWhileReconciling: true });
}

/**
* Evaluates a feature flag, returning a boolean.
* By default, components will re-render when the flag value changes.
Expand Down
Loading

0 comments on commit 6bcef89

Please sign in to comment.