What happened?
My app has an Auto / Light / Dark theme selector and a separate "True Dark" toggle that, when in dark mode, swaps much of the dark palette's grays for pure black. The toggle is implemented as a single live call:
Uniwind.updateCSSVariables("dark", { "--color-gray-900": "#000000", ... })
On 1.0.0-rc.2 and earlier this repaints every dark-gray surface immediately. From 1.0.0-rc.3 onward (verified through 1.1.2), already-mounted views keep painting with the pre-update palette and new mounts and views that re-render for other reasons get the new colors, but persistent overlays, screen backgrounds, and currently-visible list cells stay stuck until something forces them to re-render. The result is a half-applied theme.
Investigating with AI gave me this solution (including a simple patch-package that fixes the issue, though obviously using patch-package is less than ideal):
Bisected to 1.0.0-rc.3. The breaking change is in src/components/native/utils/listener.ts: when per-component-context caching was added for scoped themes, the preceding UniwindStore.invalidate() call was removed (and invalidate() itself was deleted from UniwindStore).
The new per-theme cache (this.cache[theme][className][stateMask]) is correctly bypassed on setTheme switches because lookups go to a different bucket, but on a live updateCSSVariables call against the active theme the bucket stays the same, never gets evicted, and the listener serves stale cached styles into the shadow-tree fast path.
Suggested fix: restore invalidate() on UniwindStore and call it from onResolveClassNames after reload. Doesn't conflict with the scoped-theme caching that motivated the rc.3 refactor — scoped lookups still benefit from the cache between invalidations. Verified working as a local patch-package patch against 1.1.2:
--- a/src/core/native/store.ts
+++ b/src/core/native/store.ts
@@
reload = (runtime) => { ... }
+ invalidate = () => {
+ for (const theme of Object.keys(this.cache)) {
+ this.cache[theme] = Object.create(null)
+ }
+ }
+
reinit = (...) => { ... }
--- a/src/components/native/utils/listener.ts
+++ b/src/components/native/utils/listener.ts
@@ UniwindRuntime.onResolveClassNames(({ ... }) => {
UniwindStore.reload(runtime as UniwindRuntimeCurrentPublic)
+ UniwindStore.invalidate()
const mutations = Object.fromEntries(...)
Steps to Reproduce
- In a React Native app using uniwind-pro, register a global theme (e.g.
dark) with --color-* variables.
- Mount components consuming those variables via classes like
dark:bg-gray-900.
- With the dark theme active, call
Uniwind.updateCSSVariables("dark", { "--color-gray-900": "#000000" }) (e.g. in response to a user-driven toggle).
- Observe: already-mounted views keep showing the previous color; only views that re-render afterwards reflect the new value.
Snack or Repository Link (Optional)
No response
Uniwind version
1.1.2
React Native Version
0.83.2
Platforms
iOS
Expo
Yes
Additional information
What happened?
My app has an Auto / Light / Dark theme selector and a separate "True Dark" toggle that, when in dark mode, swaps much of the dark palette's grays for pure black. The toggle is implemented as a single live call:
On
1.0.0-rc.2and earlier this repaints every dark-gray surface immediately. From1.0.0-rc.3onward (verified through1.1.2), already-mounted views keep painting with the pre-update palette and new mounts and views that re-render for other reasons get the new colors, but persistent overlays, screen backgrounds, and currently-visible list cells stay stuck until something forces them to re-render. The result is a half-applied theme.Investigating with AI gave me this solution (including a simple patch-package that fixes the issue, though obviously using patch-package is less than ideal):
Bisected to
1.0.0-rc.3. The breaking change is insrc/components/native/utils/listener.ts: when per-component-context caching was added for scoped themes, the precedingUniwindStore.invalidate()call was removed (and invalidate() itself was deleted from UniwindStore).The new per-theme cache (
this.cache[theme][className][stateMask]) is correctly bypassed on setTheme switches because lookups go to a different bucket, but on a live updateCSSVariables call against the active theme the bucket stays the same, never gets evicted, and the listener serves stale cached styles into the shadow-tree fast path.Suggested fix: restore invalidate() on UniwindStore and call it from onResolveClassNames after reload. Doesn't conflict with the scoped-theme caching that motivated the rc.3 refactor — scoped lookups still benefit from the cache between invalidations. Verified working as a local patch-package patch against 1.1.2:
Steps to Reproduce
dark) with--color-*variables.dark:bg-gray-900.Uniwind.updateCSSVariables("dark", { "--color-gray-900": "#000000" })(e.g. in response to a user-driven toggle).Snack or Repository Link (Optional)
No response
Uniwind version
1.1.2
React Native Version
0.83.2
Platforms
iOS
Expo
Yes
Additional information