diff --git a/packages/client/src/provider/in-memory-provider/in-memory-provider.ts b/packages/client/src/provider/in-memory-provider/in-memory-provider.ts index 9261830b7..7833cfda3 100644 --- a/packages/client/src/provider/in-memory-provider/in-memory-provider.ts +++ b/packages/client/src/provider/in-memory-provider/in-memory-provider.ts @@ -59,16 +59,12 @@ export class InMemoryProvider implements Provider { .filter(([key, value]) => this._flagConfiguration[key] !== value) .map(([key]) => key); - this.status = ProviderStatus.STALE; - this.events.emit(ProviderEvents.Stale); - this._flagConfiguration = { ...flagConfiguration }; this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged }); try { await this.initialize(this._context); - // we need to emit our own events in this case, since it's not part of the init flow. - this.events.emit(ProviderEvents.Ready); + this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged }); } catch (err) { this.events.emit(ProviderEvents.Error); throw err; diff --git a/packages/react/src/use-feature-flag.ts b/packages/react/src/use-feature-flag.ts index 353aa6349..e1a56177b 100644 --- a/packages/react/src/use-feature-flag.ts +++ b/packages/react/src/use-feature-flag.ts @@ -5,10 +5,16 @@ import { useOpenFeatureClient } from './provider'; type ReactFlagEvaluationOptions = { /** * Suspend flag evaluations while the provider is not ready. - * Set to false if you don't want to use React Suspense API. + * Set to false if you don't want to show suspense fallbacks util the provider is initialized. * Defaults to true. */ - suspend?: boolean, + 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. + */ + suspendWhileStale?: boolean, /** * Update the component if the provider emits a ConfigurationChanged event. * Set to false to prevent components from re-rendering when flag value changes @@ -28,7 +34,8 @@ type ReactFlagEvaluationOptions = { const DEFAULT_OPTIONS: ReactFlagEvaluationOptions = { updateOnContextChanged: true, updateOnConfigurationChanged: true, - suspend: true, + suspendUntilReady: true, + suspendWhileStale: false, }; enum SuspendState { @@ -150,37 +157,48 @@ export function useObjectFlagDetails(flagKey: s function attachHandlersAndResolve(flagKey: string, defaultValue: T, resolver: (client: Client) => (flagKey: string, defaultValue: T) => EvaluationDetails, options?: ReactFlagEvaluationOptions): EvaluationDetails { const defaultedOptions = { ...DEFAULT_OPTIONS, ...options }; const [, updateState] = useState(); + const client = useOpenFeatureClient(); const forceUpdate = () => { updateState({}); }; - const client = useOpenFeatureClient(); + const suspendRef = () => { + suspend(client, updateState, ProviderEvents.ContextChanged, ProviderEvents.ConfigurationChanged, ProviderEvents.Ready); + }; useEffect(() => { - - if (client.providerStatus !== ProviderStatus.READY) { + if (client.providerStatus === ProviderStatus.NOT_READY) { // update when the provider is ready client.addHandler(ProviderEvents.Ready, forceUpdate); - if (defaultedOptions.suspend) { - suspend(client, updateState); + if (defaultedOptions.suspendUntilReady) { + suspend(client, updateState, ProviderEvents.Ready); } } if (defaultedOptions.updateOnContextChanged) { // update when the context changes client.addHandler(ProviderEvents.ContextChanged, forceUpdate); + if (defaultedOptions.suspendWhileStale) { + client.addHandler(ProviderEvents.Stale, suspendRef); + } } - + return () => { + // cleanup the handlers + client.removeHandler(ProviderEvents.Ready, forceUpdate); + client.removeHandler(ProviderEvents.ContextChanged, forceUpdate); + client.removeHandler(ProviderEvents.Stale, suspendRef); + }; + }, []); + + useEffect(() => { if (defaultedOptions.updateOnConfigurationChanged) { // update when the provider configuration changes client.addHandler(ProviderEvents.ConfigurationChanged, forceUpdate); } return () => { - // cleanup the handlers (we can do this unconditionally with no impact) - client.removeHandler(ProviderEvents.Ready, forceUpdate); - client.removeHandler(ProviderEvents.ContextChanged, forceUpdate); + // cleanup the handlers client.removeHandler(ProviderEvents.ConfigurationChanged, forceUpdate); }; - }, [client]); + }, []); return resolver(client).call(client, flagKey, defaultValue); } @@ -189,21 +207,24 @@ function attachHandlersAndResolve(flagKey: string, defaultV * Suspend function. If this runs, components using the calling hook will be suspended. * @param {Client} client the OpenFeature client * @param {Function} updateState the state update function + * @param {ProviderEvents[]} resumeEvents list of events which will resume the suspend */ -function suspend(client: Client, updateState: Dispatch>) { +function suspend(client: Client, updateState: Dispatch>, ...resumeEvents: ProviderEvents[]) { + let suspendResolver: () => void; - let suspendRejecter: () => void; + const suspendPromise = new Promise((resolve) => { suspendResolver = () => { resolve(); - client.removeHandler(ProviderEvents.Ready, suspendResolver); // remove handler once it's run - }; - suspendRejecter = () => { - resolve(); // we still resolve here, since we don't want to throw errors - client.removeHandler(ProviderEvents.Error, suspendRejecter); // remove handler once it's run + resumeEvents.forEach((e) => { + client.removeHandler(e, suspendResolver); // remove handlers once they've run + }); + client.removeHandler(ProviderEvents.Error, suspendResolver); }; - client.addHandler(ProviderEvents.Ready, suspendResolver); - client.addHandler(ProviderEvents.Error, suspendRejecter); + 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)); }