-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: options inheritance, useWhenProviderReady, suspend by default (#…
…900) * suspends while reconciling by default * adds the ability to configure options at the context-provider level * adds new `useWhenProviderReady` hook which force-suspend components until the provider is ready (especially good for React 17 shortcomings * updates README with FAQ/troubleshooting and more --------- Signed-off-by: Todd Baert <[email protected]> Co-authored-by: Lukas Reining <[email protected]>
- Loading branch information
1 parent
37c50b7
commit 539e741
Showing
10 changed files
with
306 additions
and
149 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ReactFlagEvaluationOptions, 'suspend'>; | ||
|
||
/** | ||
* 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}), | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SetStateAction<object | undefined>>, | ||
...resumeEvents: ProviderEvents[] | ||
) { | ||
let suspendResolver: () => void; | ||
|
||
const suspendPromise = new Promise<void>((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<T>} promise to wrap | ||
* @template T flag type | ||
* @returns {Function} suspense-compliant lambda | ||
*/ | ||
export function suspenseWrapper<T>(promise: Promise<T>) { | ||
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.'); | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export * from './use-feature-flag'; | ||
export * from './use-feature-flag'; |
Oops, something went wrong.