Repository navigation
[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
Description
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) emitsUniwind.__reinit(rt => ${virtualCode}, …). Viewport and inset values compile to closures over thatrt: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, withthis.runtime, so every compiled closure holds a reference to that object. -
On every native runtime change,
components/native/utils/listener.tscallsUniwindStore.reload(runtime)and thenUniwindListener.notify(dependencies).reload()replacesthis.runtimeinstead of updating it:reload = (runtime: UniwindRuntimeCurrentPublic) => { this._fontScale = runtime.fontScale this._pixelRatio = runtime.pixelRatio this.runtime = { ...runtime, ...this.jsFunctions, } }
-
Breakpoint matching in
resolveStylesreadsthis.runtime.screen.widthdirectly, somd:/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 stalert.
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):
- The script compiles
global.cssfor iOS with the Metro transformer's owncompileCSSand prints the compiled entries forw-[50vw]andpt-safe. Both close overrt. - It builds the stylesheet callback exactly as Metro emits it and passes it to the real
UniwindStore.reinit()at launch: 834×1210, portrait. - It replays what
listener.tsdoes on a native change,reload(runtime)thennotify(dependencies), for a rotation to 1210×834 landscape and then a top-inset change from 24 to 44. - 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 〰
- I've searched for similar issues in this repository and found none
- I am a Uniwind Pro user
Activity
jpudysz commentedon Sep 28, 2026
Reproduced & fixed. Will be a part of next release.