What happened?
The theme-flip lane keys its style mutations by class string alone: getMutationKey (components/native/utils/listener.ts) builds the key from className + scopedTheme + variablesCacheKey, dropping the per-node state and dataAttributes that were used to resolve each registered node's styles. Siblings that share one class string but differ in data-* values therefore collapse into a single mutation — last writer wins — and a theme flip hands every sibling one arbitrary sibling's style.
In the linked repro: three pills share rounded-full px-4 py-2 bg-fill data-[selected=true]:bg-brand, differing only in data-selected (one true, two false). After a flip that lands while the screen is frozen (freezeOnBlur), all three pills come back bg-brand — the two data-selected={false} pills received the selected sibling's payload. Screenshot-obvious, fires every time. The active:/focus:/disabled: state lane collapses the same way for the same reason (the key drops state too).
Frozen subtrees make it sticky because they never receive a repairing React commit; on a visible screen the next render heals it. In our production app this presented as pills going white-on-white after a cross-tab flip; we've since converted 19 call sites from data-* variants to per-state class strings (cn ternaries / tv variants) as a workaround, which is why our app no longer shows it — but the engine behavior remains.
Suggested direction: include the resolved dataAttributes/state in the mutation key (or key mutations per registered node rather than per string).
No React Compiler involvement in this one — unrelated to #655's ingredient list, though it lives on the same repro repo. Sibling of #649/#655 in the native className lane.
Steps to Reproduce
In the linked repo (bun install && npx expo run:ios):
- App boots in dark — tap the data-* button on the Home screen
- Pill Alpha is brand blue (
data-selected=true); Beta and Gamma carry the dark bg-fill — all three share one class string
- Tap "Open Settings" (the screen freezes under
freezeOnBlur)
- Tap "Light", then "Back"
- All three pills are now
bg-brand blue — the two data-selected={false} pills received the selected sibling's flip payload
Snack or Repository Link (Optional)
https://github.com/invivek26/uniwind-frozen-swap-repro
Uniwind version
uniwind-pro 1.6.0
React Native Version
0.85.3
Platforms
iOS
Expo
Yes
Additional information 〰
What happened?
The theme-flip lane keys its style mutations by class string alone:
getMutationKey(components/native/utils/listener.ts) builds the key fromclassName + scopedTheme + variablesCacheKey, dropping the per-nodestateanddataAttributesthat were used to resolve each registered node's styles. Siblings that share one class string but differ indata-*values therefore collapse into a single mutation — last writer wins — and a theme flip hands every sibling one arbitrary sibling's style.In the linked repro: three pills share
rounded-full px-4 py-2 bg-fill data-[selected=true]:bg-brand, differing only indata-selected(one true, two false). After a flip that lands while the screen is frozen (freezeOnBlur), all three pills come backbg-brand— the twodata-selected={false}pills received the selected sibling's payload. Screenshot-obvious, fires every time. Theactive:/focus:/disabled:state lane collapses the same way for the same reason (the key dropsstatetoo).Frozen subtrees make it sticky because they never receive a repairing React commit; on a visible screen the next render heals it. In our production app this presented as pills going white-on-white after a cross-tab flip; we've since converted 19 call sites from
data-*variants to per-state class strings (cnternaries /tvvariants) as a workaround, which is why our app no longer shows it — but the engine behavior remains.Suggested direction: include the resolved
dataAttributes/statein the mutation key (or key mutations per registered node rather than per string).No React Compiler involvement in this one — unrelated to #655's ingredient list, though it lives on the same repro repo. Sibling of #649/#655 in the native className lane.
Steps to Reproduce
In the linked repo (
bun install && npx expo run:ios):data-selected=true); Beta and Gamma carry the darkbg-fill— all three share one class stringfreezeOnBlur)bg-brandblue — the twodata-selected={false}pills received the selected sibling's flip payloadSnack or Repository Link (Optional)
https://github.com/invivek26/uniwind-frozen-swap-repro
Uniwind version
uniwind-pro 1.6.0
React Native Version
0.85.3
Platforms
iOS
Expo
Yes
Additional information 〰