fix(react-sdk): don't mask existing flag values while client is initializing#667
Open
linardsblk wants to merge 1 commit into
Open
fix(react-sdk): don't mask existing flag values while client is initializing#667linardsblk wants to merge 1 commit into
linardsblk wants to merge 1 commit into
Conversation
…alizing useFlag returned isEnabled: false and an empty config for every flag whenever the client state was "initializing", even when the client already held servable values. This flashes flag-gated UI to defaults: - ReflagBootstrappedProvider: initialize() passes through "initializing" after hydration although flags were injected at construction, so every SSR-bootstrapped page gets one guaranteed frame with all flags disabled - defeating the purpose of bootstrapping. - setContext(): the initialized -> initializing -> initialized transition around the refetch masked last-known values for the whole request duration on every context change. Only fall back to disabled defaults when the client has no value for the flag. isLoading is still exposed unchanged for consumers that want to render loading states themselves. The new regression test records every render, since waitFor-based assertions skip the transient frames where the flash happened.
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(react-sdk): don't mask existing flag values while client is initializing
Problem
useFlagreturnsisEnabled: falseand an emptyconfigfor every flag whenever the provider reportsisLoading— even when the client already holds servable values:This bites in two places:
ReflagBootstrappedProvider(SSR): the client is constructed with bootstrapped flags and serves them from the first render — but the post-mountinitialize()call unconditionally passes through the"initializing"state (SSE/pubsub setup etc.), flippingisLoadingtotruefor at least one committed frame. During that frameuseFlagmasks the valid bootstrapped values, so every SSR-bootstrapped page renders: correct values → all flags disabled → correct values. Flag-gated UI visibly flashes, which is exactly what bootstrapping exists to prevent. Notably,ReflagBootstrappedProviderdefaultsinitialLoadingtofalse— the intent that a bootstrapped client isn't loading is already there; thestateUpdatedlistener overrides it one effect-tick later.setContext(): theinitialized → "initializing" → initializedtransition around the context refetch masks last-known values for the whole request duration on every context change (e.g. auth session resolving after mount), instead of serving them stale-while-revalidate style.Repro for (1): any page using
ReflagBootstrappedProviderwithgetFlagsForBootstrap()data from@reflag/node-sdk; loguseFlag(...).isEnabledand observetrue → false → trueacross hydration.Fix
Fall back to disabled defaults only when the client has no value for the flag (
!flag).isLoadingis still computed and returned unchanged, so consumers that render skeletons or guard redirects on it keep working exactly as before. The only behavioral change: when a value exists, it is served regardless of the client's lifecycle state.Test
The existing bootstrapped-provider tests assert via
waitFor, which skips transient frames — the flash was invisible to them. The new regression test mounts a probe component that recordsisEnabled/isLoadingon every render underReflagBootstrappedProvider, waits for initialization to settle, and asserts no render ever reported the bootstrapped flag as disabled. It fails on the previous implementation and passes with this change.Out of scope (flagging for maintainers)
Two adjacent symptoms share the same root cause (lifecycle state conflated with data readiness) and are not changed here, to keep this PR minimal and non-breaking:
loadingComponentpassed toReflagBootstrappedProviderstill swaps in for the one"initializing"frame after hydration.useIsLoading()still readstrueduring that frame.A cleaner long-term shape might be separating lifecycle state from a data-readiness signal (e.g.
ready/hasFlags), but"initializing"is currently load-bearing (concurrent-initialize()dedupe,setContexttransitions, pubsub reconcile checks, the publicstateUpdatedevent sequence), so that felt like a maintainer decision rather than a drive-by change. Happy to follow up if there's interest.