Skip to content

fix(native): one resolved-style cache key per distinct input set - #434

Open
YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/mapping-config-identity
Open

YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/mapping-config-identity

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Problem

generateStateHash keys the resolved-style cache on weak-key identity, so it shares an entry only when equal inputs arrive as one object. Two defects on main break that:

  1. The config is minted per instance. useCssElement derives one from a module constant on every mount, so N identical elements each hold their own cache entry, sorted rule array and observable.
  2. The key is a lossy digest. Folding the key numbers into one base-36 number collides, and family returns the entry it holds whatever rules the second caller brought, so a collision renders one element's styles on another.

Solution

  • The config is derived once per mapping through weakFamily. A mapping that is not an object is refused by name, rather than failing inside the WeakMap with a message that names neither styled() nor the argument.
  • The key numbers are sorted and joined, which is exact and order-independent. They sit in a Float64Array, so the sort is numeric without a comparator and the unbounded key counter does not wrap.
  • family gains size(), so the invariant is assertable.

Tests

  • config-identity.test.ts and state-hash.test.ts pin the derivation and the key: one mapping gives one config, a mapping that is not an object is refused by name, distinct key sets never share a key, and order does not matter.
  • style-cache.test.tsx counts entries for identical and differing elements, and checks that elements sharing an entry and its config each see the value nativeStyleMapping took.
  • On main with only size() added, 8 of the 14 cases fail: 50 identical elements hold 50 entries, and three identical buttons hold three. Each of 6 mutations fails its own cases.

Verification

On Windows with Node 26:

  • yarn lint clean
  • yarn typecheck clean
  • yarn test --coverage: every failure also fails on main on this machine (the four babel cases)
  • yarn build clean
  • yarn example expo export --platform web exported

Merge order

#436 builds on this, so this lands first.

It also shares lines with #412 (native/reactivity.ts); whichever lands second rebases.

Base

Re-written on main (a5002c5). #451 makes family and weakFamily cache on presence, which this fixed too, so that half is gone.

@YevheniiKotyrlo YevheniiKotyrlo changed the title fix(native): equal inputs produce one resolved-style cache key fix(native): equal inputs produce one resolved-style cache key, and distinct inputs never share one Aug 19, 2026
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/mapping-config-identity branch from c877b35 to 7bc057a Compare August 19, 2026 17:51
@YevheniiKotyrlo YevheniiKotyrlo changed the title fix(native): equal inputs produce one resolved-style cache key, and distinct inputs never share one fix(native): one resolved-style cache key per distinct input set Aug 19, 2026
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/mapping-config-identity branch 2 times, most recently from 18b5d2b to 152b5ad Compare August 20, 2026 04:55
@YevheniiKotyrlo

YevheniiKotyrlo commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor Author

No device frame

The change is how many cache entries identical elements share, not what they paint, and the description's tests count them.

generateStateHash keys the resolved-style cache on weak-key identity, so it
shares only when equal inputs arrive as one object, and two defects broke
that on main.

The config was minted per instance: useCssElement derived one from a module
constant on every mount, so N identical elements each held their own cache
entry, sorted rule array and observable. It is now derived once per mapping
through weakFamily, and a mapping that is not an object is refused by name
rather than failing inside the WeakMap.

The key was lossy: folding the key numbers into one and printing it base-36
is a digest, and family returns the entry it holds whatever rules the second
caller brought, so a collision rendered one element's styles on another. The
numbers are now sorted and joined, which is exact and order-independent, in
a Float64Array so the sort is native and the unbounded key counter does not
wrap. family gains size() so the invariant is assertable.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant