Skip to content

Live Uniwind.updateCSSVariables leaves resolved-style cache stale (regression in 1.0.0-rc.3) #533

@jessejanderson

Description

@jessejanderson

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

  1. In a React Native app using uniwind-pro, register a global theme (e.g. dark) with --color-* variables.
  2. Mount components consuming those variables via classes like dark:bg-gray-900.
  3. With the dark theme active, call Uniwind.updateCSSVariables("dark", { "--color-gray-900": "#000000" }) (e.g. in response to a user-driven toggle).
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions