diff --git a/packages/react/README.md b/packages/react/README.md
index 915b511b8..4ee796083 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -50,10 +50,13 @@ In addition to the feature provided by the [web sdk](https://openfeature.dev/doc
- [yarn](#yarn)
- [Required peer dependencies](#required-peer-dependencies)
- [Usage](#usage)
- - [Multiple Providers and Domains](#multiple-providers-and-domains)
- - [Re-rendering with Context Changes](#re-rendering-with-context-changes)
- - [Re-rendering with Flag Configuration Changes](#re-rendering-with-flag-configuration-changes)
- - [Suspense Support](#suspense-support)
+ - [OpenFeatureProvider context provider](#openfeatureprovider-context-provider)
+ - [Evaluation hooks](#evaluation-hooks)
+ - [Multiple Providers and Domains](#multiple-providers-and-domains)
+ - [Re-rendering with Context Changes](#re-rendering-with-context-changes)
+ - [Re-rendering with Flag Configuration Changes](#re-rendering-with-flag-configuration-changes)
+ - [Suspense Support](#suspense-support)
+- [FAQ and troubleshooting](#faq-and-troubleshooting)
- [Resources](#resources)
## Quick start
@@ -87,7 +90,9 @@ The following list contains the peer dependencies of `@openfeature/react-sdk` wi
### Usage
-The `OpenFeatureProvider` represents a scope for feature flag evaluations within a React application.
+#### OpenFeatureProvider context provider
+
+The `OpenFeatureProvider` is a [React context provider](https://react.dev/reference/react/createContext#provider) which represents a scope for feature flag evaluations within a React application.
It binds an OpenFeature client to all evaluations within child components, and allows the use of evaluation hooks.
The example below shows how to use the `OpenFeatureProvider` with OpenFeature's `InMemoryProvider`.
@@ -123,9 +128,15 @@ function App() {
);
}
+```
+
+#### Evaluation hooks
+Within the provider, you can use the various evaluation hooks to evaluate flags.
+
+```tsx
function Page() {
- // Use the "query-style" flag evaluation hook.
+ // Use the "query-style" flag evaluation hook, specifying a flag-key and a default value.
const { value: showNewMessage } = useFlag('new-message', true);
return (
@@ -135,9 +146,8 @@ function Page() {
)
}
-
-export default App;
```
+
You can use the strongly-typed flag value and flag evaluation detail hooks as well, if you prefer.
```tsx
@@ -159,8 +169,7 @@ const {
} = useBooleanFlagDetails('new-message', false);
```
-### Multiple Providers and Domains
-
+#### Multiple Providers and Domains
Multiple providers can be used by passing a `domain` to the `OpenFeatureProvider`:
@@ -183,11 +192,11 @@ OpenFeature.getClient('my-domain');
For more information about `domains`, refer to the [web SDK](https://github.com/open-feature/js-sdk/blob/main/packages/client/README.md).
-### Re-rendering with Context Changes
+#### Re-rendering with Context Changes
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 hook options:
+You can disable this feature in the hook options (or in the [OpenFeatureProvider](#openfeatureprovider-context-provider)):
```tsx
function Page() {
@@ -200,11 +209,11 @@ function Page() {
For more information about how evaluation context works in the React SDK, see the documentation on OpenFeature's [static context SDK paradigm](https://openfeature.dev/specification/glossary/#static-context-paradigm).
-### Re-rendering with Flag Configuration Changes
+#### Re-rendering with Flag Configuration Changes
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 hook options:
+You can disable this feature in the hook options (or in the [OpenFeatureProvider](#openfeatureprovider-context-provider)):
```tsx
function Page() {
@@ -217,11 +226,11 @@ function Page() {
Note that if your provider doesn't support updates, this configuration has no impact.
-### Suspense Support
+#### Suspense Support
Frequently, providers need to perform some initial startup tasks.
-It may be desireable not to display components with feature flags until this is complete.
-Built-in [suspense](https://react.dev/reference/react/Suspense) support makes this easy:
+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.
```tsx
function Content() {
@@ -252,6 +261,37 @@ function Fallback() {
// component to render before READY.
return
Waiting for provider to be ready...
;
}
+
+```
+
+This can be disabled in the hook options (or in the [OpenFeatureProvider](#openfeatureprovider-context-provider)).
+
+## FAQ and troubleshooting
+
+> I get an error that says something like: `A React component suspended while rendering, but no fallback UI was specified.`
+
+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).
+
+> I get odd rendering issues, or errors when components mount, if I use the suspense features.
+
+In React 16/17's "Legacy Suspense", when a component suspends, its sibling components initially mount and then are hidden.
+This can cause surprising effects and inconsistencies if sibling components are rendered while the provider is still getting ready.
+To fix this, you can upgrade to React 18, which uses "Concurrent Suspense", in which siblings are not mounted until their suspended sibling resolves.
+Alternatively, if you cannot upgrade to React 18, you can use the `useWhenProviderReady` utility hook in any sibling components to prevent them from mounting until the provider is ready.
+
+> I am using multiple `OpenFeatureProvider` contexts, but they are sharing the same provider or evaluation context. Why?
+
+The `OpenFeatureProvider` binds a `client` to all child components, but the provider and context associated with that client is controlled by the `domain` parameter.
+This is consistent with all OpenFeature SDKs.
+To scope an OpenFeatureProvider to a particular provider/context set the `domain` parameter on your `OpenFeatureProvider`:
+
+```tsx
+
+
+
```
## Resources
diff --git a/packages/react/src/common/options.ts b/packages/react/src/common/options.ts
new file mode 100644
index 000000000..d950c7e4a
--- /dev/null
+++ b/packages/react/src/common/options.ts
@@ -0,0 +1,77 @@
+import { FlagEvaluationOptions } from '@openfeature/web-sdk';
+
+export type ReactFlagEvaluationOptions = ({
+ /**
+ * Enable or disable all suspense functionality.
+ * Cannot be used in conjunction with `suspendUntilReady` and `suspendWhileReconciling` options.
+ */
+ suspend?: boolean;
+ suspendUntilReady?: never;
+ suspendWhileReconciling?: never;
+} | {
+ /**
+ * 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.
+ * Cannot be used in conjunction with `suspend` option.
+ */
+ 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.
+ * Cannot be used in conjunction with `suspend` option.
+ */
+ suspendWhileReconciling?: boolean;
+ suspend?: never;
+}) & {
+ /**
+ * Update the component if the provider emits a ConfigurationChanged event.
+ * Set to false to prevent components from re-rendering when flag value changes
+ * are received by the associated provider.
+ * Defaults to true.
+ */
+ updateOnConfigurationChanged?: boolean;
+ /**
+ * Update the component when the OpenFeature context changes.
+ * Set to false to prevent components from re-rendering when attributes which
+ * may be factors in flag evaluation change.
+ * Defaults to true.
+ */
+ updateOnContextChanged?: boolean;
+} & FlagEvaluationOptions;
+
+export type NormalizedOptions = Omit;
+
+/**
+ * Default options.
+ * DO NOT EXPORT PUBLICLY
+ * @internal
+ */
+export const DEFAULT_OPTIONS: ReactFlagEvaluationOptions = {
+ updateOnContextChanged: true,
+ updateOnConfigurationChanged: true,
+ suspendUntilReady: true,
+ suspendWhileReconciling: true,
+};
+
+/**
+ * Returns normalization options (all `undefined` fields removed, and `suspend` decomposed to `suspendUntilReady` and `suspendWhileReconciling`).
+ * DO NOT EXPORT PUBLICLY
+ * @internal
+ * @param {ReactFlagEvaluationOptions} options options to normalize
+ * @returns {NormalizedOptions} normalized options
+ */
+export const normalizeOptions: (options?: ReactFlagEvaluationOptions) => NormalizedOptions = (options?: ReactFlagEvaluationOptions) => {
+ const defaultOptionsIfMissing = !options ? {} : options;
+ // fall-back the suspense options
+ const suspendUntilReady = 'suspendUntilReady' in defaultOptionsIfMissing ? defaultOptionsIfMissing.suspendUntilReady : defaultOptionsIfMissing.suspend;
+ const suspendWhileReconciling = 'suspendWhileReconciling' in defaultOptionsIfMissing ? defaultOptionsIfMissing.suspendWhileReconciling : defaultOptionsIfMissing.suspend;
+ return {
+ updateOnContextChanged: defaultOptionsIfMissing.updateOnContextChanged,
+ updateOnConfigurationChanged: defaultOptionsIfMissing.updateOnConfigurationChanged,
+ // only return these if properly set (no undefined to allow overriding with spread)
+ ...(typeof suspendUntilReady === 'boolean' && {suspendUntilReady}),
+ ...(typeof suspendWhileReconciling === 'boolean' && {suspendWhileReconciling}),
+ };
+};
diff --git a/packages/react/src/common/suspend.ts b/packages/react/src/common/suspend.ts
new file mode 100644
index 000000000..c598af0b3
--- /dev/null
+++ b/packages/react/src/common/suspend.ts
@@ -0,0 +1,75 @@
+import { Client, ProviderEvents } from '@openfeature/web-sdk';
+import { Dispatch, SetStateAction } from 'react';
+
+enum SuspendState {
+ Pending,
+ Success,
+ Error,
+}
+
+/**
+ * Suspend function. If this runs, components using the calling hook will be suspended.
+ * DO NOT EXPORT PUBLICLY
+ * @internal
+ * @param {Client} client the OpenFeature client
+ * @param {Function} updateState the state update function
+ * @param {ProviderEvents[]} resumeEvents list of events which will resume the suspend
+ */
+export function suspend(
+ client: Client,
+ updateState: Dispatch>,
+ ...resumeEvents: ProviderEvents[]
+) {
+ let suspendResolver: () => void;
+
+ const suspendPromise = new Promise((resolve) => {
+ suspendResolver = () => {
+ resolve();
+ resumeEvents.forEach((e) => {
+ client.removeHandler(e, suspendResolver); // remove handlers once they've run
+ });
+ client.removeHandler(ProviderEvents.Error, suspendResolver);
+ };
+ resumeEvents.forEach((e) => {
+ client.addHandler(e, suspendResolver);
+ });
+ client.addHandler(ProviderEvents.Error, suspendResolver); // we never want to throw, resolve with errors - we may make this configurable later
+ });
+ updateState(suspenseWrapper(suspendPromise));
+}
+
+/**
+ * Promise wrapper that throws unresolved promises to support React suspense.
+ * DO NOT EXPORT PUBLICLY
+ * @internal
+ * @param {Promise} promise to wrap
+ * @template T flag type
+ * @returns {Function} suspense-compliant lambda
+ */
+export function suspenseWrapper(promise: Promise) {
+ let status: SuspendState = SuspendState.Pending;
+ let result: T;
+
+ const suspended = promise
+ .then((value) => {
+ status = SuspendState.Success;
+ result = value;
+ })
+ .catch((error) => {
+ status = SuspendState.Error;
+ result = error;
+ });
+
+ return () => {
+ switch (status) {
+ case SuspendState.Pending:
+ throw suspended;
+ case SuspendState.Success:
+ return result;
+ case SuspendState.Error:
+ throw result;
+ default:
+ throw new Error('Suspending promise is in an unknown state.');
+ }
+ };
+}
diff --git a/packages/react/src/evaluation/index.ts b/packages/react/src/evaluation/index.ts
index 5868641fa..9a5edb703 100644
--- a/packages/react/src/evaluation/index.ts
+++ b/packages/react/src/evaluation/index.ts
@@ -1 +1 @@
-export * from './use-feature-flag';
\ No newline at end of file
+export * from './use-feature-flag';
diff --git a/packages/react/src/evaluation/use-feature-flag.ts b/packages/react/src/evaluation/use-feature-flag.ts
index 03e8e393d..028b26310 100644
--- a/packages/react/src/evaluation/use-feature-flag.ts
+++ b/packages/react/src/evaluation/use-feature-flag.ts
@@ -1,59 +1,19 @@
import {
Client,
EvaluationDetails,
- FlagEvaluationOptions,
FlagValue,
JsonValue,
ProviderEvents,
ProviderStatus,
- StandardResolutionReasons,
+ StandardResolutionReasons
} from '@openfeature/web-sdk';
-import { Dispatch, SetStateAction, useEffect, useState } from 'react';
-import { useOpenFeatureClient } from '../provider';
+import { useEffect, useState } from 'react';
+import { DEFAULT_OPTIONS, ReactFlagEvaluationOptions, normalizeOptions } from '../common/options';
+import { suspend } from '../common/suspend';
+import { useProviderOptions } from '../provider/context';
+import { useOpenFeatureClient } from '../provider/use-open-feature-client';
import { FlagQuery } from '../query';
-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.
- */
- 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 false.
- */
- suspendWhileReconciling?: boolean;
- /**
- * Update the component if the provider emits a ConfigurationChanged event.
- * Set to false to prevent components from re-rendering when flag value changes
- * are received by the associated provider.
- * Defaults to true.
- */
- updateOnConfigurationChanged?: boolean;
- /**
- * Update the component when the OpenFeature context changes.
- * Set to false to prevent components from re-rendering when attributes which
- * may be factors in flag evaluation change.
- * Defaults to true.
- */
- updateOnContextChanged?: boolean;
-} & FlagEvaluationOptions;
-
-const DEFAULT_OPTIONS: ReactFlagEvaluationOptions = {
- updateOnContextChanged: true,
- updateOnConfigurationChanged: true,
- suspendUntilReady: true,
- suspendWhileReconciling: false,
-};
-
-enum SuspendState {
- Pending,
- Success,
- Error,
-}
-
// This type is a bit wild-looking, but I think we need it.
// We have to use the conditional, because otherwise useFlag('key', false) would return false, not boolean (too constrained).
// We have a duplicate for the hook return below, this one is just used for casting because the name isn't as clear
@@ -284,7 +244,8 @@ function attachHandlersAndResolve(
resolver: (client: Client) => (flagKey: string, defaultValue: T) => EvaluationDetails,
options?: ReactFlagEvaluationOptions,
): EvaluationDetails {
- const defaultedOptions = { ...DEFAULT_OPTIONS, ...options };
+ // highest priority > evaluation hook options > provider options > default options > lowest priority
+ const defaultedOptions = { ...DEFAULT_OPTIONS, ...useProviderOptions(), ...normalizeOptions(options)};
const [, updateState] = useState