Skip to content

[Pro 1.7.0 / iOS] vw/vh and safe-area styles freeze at their launch values after rotation — UniwindStore.reload() replaces the runtime object the compiled styles closed over #693

Closed

Description

@invivek26

What happened?

After the device rotates, every vw/vh- and safe-area-based style keeps the value it resolved at app launch. w-[calc(100vw-24px)] rows, headers and buttons stay at their portrait width in landscape, and safe-area classes keep the launch insets. Breakpoint variants on the same screens (md:, lg:) switch correctly, and styles sized from React Native's useWindowDimensions are unaffected. Relaunching in the new orientation gives the correct values until the next rotation. Split View resizes and inset changes go through the same reload() path.

We hit it on an 11-inch iPad Pro (834×1210, iPadOS 26.5) after launching in portrait: a hidden w-[100vw] probe we keep in production reported 834 against a window width of 1210.

Mechanism (read from the shipped 1.7.0 sources):

  • The Metro transformer (src/bundler/adapters/metro/transformer.ts) emits Uniwind.__reinit(rt => ${virtualCode}, …). Viewport and inset values compile to closures over that rt: w-[50vw] → function(vars) { return rt.screen.width * 0.5 }, pt-safe → function(vars) { return rt.insets.top }.

  • UniwindStore.reinit() (src/core/native/store.ts) calls that callback once, with this.runtime, so every compiled closure holds a reference to that object.

  • On every native runtime change, components/native/utils/listener.ts calls UniwindStore.reload(runtime) and then UniwindListener.notify(dependencies). reload() replaces this.runtime instead of updating it:

    reload = (runtime: UniwindRuntimeCurrentPublic) => {
        this._fontScale = runtime.fontScale
        this._pixelRatio = runtime.pixelRatio
        this.runtime = {
            ...runtime,
            ...this.jsFunctions,
        }
    }
  • Breakpoint matching in resolveStyles reads this.runtime.screen.width directly, so md:/lg: see the new object. The compiled closures keep reading the launch object. notify() does clear the style cache, so styles are recomputed after the rotation, but from the stale rt.

OSS uniwind 1.12.0 is not affected: its Dimensions listener assigns UniwindStore.runtime.screen and .orientation on the existing object, which the closures share.

Suggested fix: keep the runtime object's identity in reload(), e.g. Object.assign(this.runtime, runtime, this.jsFunctions), or have the generated callback read the runtime lazily. We verified the Object.assign version against the repro below, and it fixes rotation in our app via a postinstall patch.

reload() is shared JS, so Android should be affected the same way on rotation or inset changes; we have only verified iOS. Related to #665 only by symptom (vw values going stale); that one was a 0×0 native sample while backgrounded, fixed in 1.7.0.

Steps to Reproduce

In the linked repo (bun install && bun repro.ts; no simulator or native build needed):

  1. The script compiles global.css for iOS with the Metro transformer's own compileCSS and prints the compiled entries for w-[50vw] and pt-safe. Both close over rt.
  2. It builds the stylesheet callback exactly as Metro emits it and passes it to the real UniwindStore.reinit() at launch: 834×1210, portrait.
  3. It replays what listener.ts does on a native change, reload(runtime) then notify(dependencies), for a rotation to 1210×834 landscape and then a top-inset change from 24 to 44.
  4. It repeats steps 2–3 with reload() mutating the runtime in place.

Everything under node_modules/uniwind runs unmodified. Only react-native, react-native-reanimated and react-native-nitro-modules are stubbed, because the store runs outside a React Native runtime.

Compiled entries (each closes over the `rt` passed to __reinit):
  w-[50vw]  function(vars) { return rt.screen.width * 0.5 }
  pt-safe   function(vars) { return rt.insets.top }

== Uniwind Pro 1.7.0 as shipped
                            w-[50vw]       pt-safe        lg:opacity-50
  launch, portrait 834      417            24             -
  rotated, landscape 1210   417            24             0.5
  status bar grows to 44    417            24             0.5

== reload() mutating the runtime in place
                            w-[50vw]       pt-safe        lg:opacity-50
  launch, portrait 834      417            24             -
  rotated, landscape 1210   605            24             0.5
  status bar grows to 44    605            44             0.5

Expected: after rotating, w-[50vw] resolves to 605, and after the inset change pt-safe resolves to 44, just as lg:opacity-50 switches on.
Actual: both keep their launch values (417 and 24) for the lifetime of the JS runtime.

On a device: launch on an iPad in portrait with any w-[100vw] view, rotate to landscape, and the view stays at the portrait width while md:/lg: styles on the same screen update.

Snack or Repository Link (Optional)

https://github.com/invivek26/uniwind-runtime-reload-repro

Uniwind version

uniwind-pro 1.7.0

React Native Version

0.86.3 (Expo SDK 57)

Platforms

iOS

Expo

Yes

Additional information 〰

Activity

  1. added
    pending-releaseFixed, waiting for new version to be released
    and removed on Sep 28, 2026
  2. jpudysz commented on Sep 28, 2026

    @jpudysz
    Collaborator

    Reproduced & fixed. Will be a part of next release.

  3. self-assigned this
    on Sep 28, 2026
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