Skip to content

Theme-flip mutations keyed by class string alone — siblings with different data-*/state values collapse to one payload (Pro 1.6.0) #656

Description

@invivek26

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):

  1. App boots in dark — tap the data-* button on the Home screen
  2. Pill Alpha is brand blue (data-selected=true); Beta and Gamma carry the dark bg-fill — all three share one class string
  3. Tap "Open Settings" (the screen freezes under freezeOnBlur)
  4. Tap "Light", then "Back"
  5. 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 〰

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions