You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A deterministic follow-up to #649, but in the native className lane (so #650 does not cover it): after a theme flip that lands while a screen is frozen (react-freeze via native-stack/tabs), the first className swap on a node of that screen — a plain cn/tv conditional flipping between two class strings — paints with no resolved color at all. On Text that means React Native's default black ink.
Pixel evidence from the repro (iOS 26 simulator, iPhone 17 Pro): a segmented pill whose selected variant is bg-brand text-on-brand, where --color-on-brand: #ffffff in the base @theme block and both theme variants. After the frozen-time dark→light flip, tapping the never-yet-selected pill paints:
pill background: rgb(0,57,228) — correct brand blue (this one is resolved with useResolveClassNames into a style object for a non-uniwind native view, i.e. the pure JS lane — always correct)
label glyphs: rgb(0,0,1) — not dark's value, not light's value, and the token is identical in every theme block. This is RN's default text color: the style landed empty, not wrong-theme
Observed behavior matrix from the same session (all in one commit sequence, minutes after the flip, so no hydration race):
Pill Y swaps to the selected string (first time this node carries it) → label black
Pill X swaps to the unselected string in the same commit → fully correct (#2d2d2d, light foreground)
Next swap: X back to selected → correct white; Y to unselected stays black (should be #2d2d2d)
One more swap: Y back to selected → heals, correct white from then on
So the broken node stays broken through one more className change and recovers on the second — an erratic warm-up rather than a stable wrong value. Flips performed while the screen is visible don't reproduce; strings that were rendered at least once under the boot theme survive flips fine (the flip lane itself re-commits mounted classNames correctly).
Where it smells from reading the source, offered with appropriate humility:
HybridShadowRegistry::updateShadowTree seeds family->nativeProps_DEPRECATED for suspended/frozen families, and that stash is cleared only in UniwindStore::linkShadowNodeWithUniwindStyle — which runs from the component's ref callback, i.e. after the commit that carried the new className. The clearing comment there already mentions "timing conflicts". A stale stash merging over the swap commit (or a merge payload the RawProps parser rejects, nulling color) would produce exactly a default-ink paint.
Two registered themes, with at least one color token whose value is identical in both (e.g. --color-on-brand: #ffffff in base + both variants).
A tab/stack screen (frozen on blur) renders segmented pills via a tv/cn conditional: selected → bg-brand text-on-brand, unselected → bg-glass-fill text-foreground. Pill X starts selected, pill Y unselected.
Cold-start the app in dark.
Navigate away so the screen freezes.
Uniwind.setTheme('light').
Navigate back — the whole screen is correctly re-themed (the flip lane works).
Tap pill Y. Its label renders with RN default black ink (no color); its background and every other node in the commit are correct.
Tap X, then Y again — Y's label stays black through the first swap and heals on the second.
What happened?
A deterministic follow-up to #649, but in the native className lane (so #650 does not cover it): after a theme flip that lands while a screen is frozen (react-freeze via native-stack/tabs), the first className swap on a node of that screen — a plain
cn/tvconditional flipping between two class strings — paints with no resolved color at all. OnTextthat means React Native's default black ink.Pixel evidence from the repro (iOS 26 simulator, iPhone 17 Pro): a segmented pill whose selected variant is
bg-brand text-on-brand, where--color-on-brand: #ffffffin the base@themeblock and both theme variants. After the frozen-time dark→light flip, tapping the never-yet-selected pill paints:rgb(0,57,228)— correct brand blue (this one is resolved withuseResolveClassNamesinto a style object for a non-uniwind native view, i.e. the pure JS lane — always correct)rgb(0,0,1)— not dark's value, not light's value, and the token is identical in every theme block. This is RN's default text color: the style landed empty, not wrong-themeObserved behavior matrix from the same session (all in one commit sequence, minutes after the flip, so no hydration race):
#2d2d2d, light foreground)#2d2d2d)So the broken node stays broken through one more className change and recovers on the second — an erratic warm-up rather than a stable wrong value. Flips performed while the screen is visible don't reproduce; strings that were rendered at least once under the boot theme survive flips fine (the flip lane itself re-commits mounted classNames correctly).
Where it smells from reading the source, offered with appropriate humility:
HybridShadowRegistry::updateShadowTreeseedsfamily->nativeProps_DEPRECATEDfor suspended/frozen families, and that stash is cleared only inUniwindStore::linkShadowNodeWithUniwindStyle— which runs from the component's ref callback, i.e. after the commit that carried the new className. The clearing comment there already mentions "timing conflicts". A stale stash merging over the swap commit (or a merge payload the RawProps parser rejects, nullingcolor) would produce exactly a default-ink paint.RuntimeChangeSource.Userflips,updateTreeis deferred behindrequestIdleCallback(components/native/utils/listener.ts). We also run theupdateCSSVariables(theme, {})re-commit workaround from [Pro 1.1.0 / iOS] bg-* views stuck on previous theme after OS appearance flip + react-navigation native-stack push/pop #518 shortly after each flip, andsetPendingCommitreplaces any pending commit — if the idle-deferred update and the recommit interleave, half-applied state seems reachable.Steps to Reproduce
--color-on-brand: #ffffffin base + both variants).tv/cnconditional: selected →bg-brand text-on-brand, unselected →bg-glass-fill text-foreground. Pill X starts selected, pill Y unselected.Uniwind.setTheme('light').Sketch of the component shape (illustrative):
Happy to run instrumented builds against our reproduction if that helps — it triggers on the first try with the steps above.
Snack or Repository Link (Optional)
No response
Uniwind version
uniwind-pro 1.6.0
React Native Version
0.85.3
Platforms
iOS
Expo
Yes
Additional information 〰