The import '../global.css'; statement in a expo router layout will invalidate on any changes in the project even if no classnames are changed. In my complex project that lead to the whole app being rerendered and loosing expo router navigation state (going back to the home page). I provide a minimal example here without the navigation reset problem but demonstrating the invalidation of the global.css which I think should be fixed anyway. If the generated styles dont change it should not invalidate.
Disclaimer: This reproduction and bug ticket was created with Claude Opus 5.
What happened?
Importing uniwind's CSS entry makes the importing module be re-executed on every Fast Refresh, even when the edited file has no import relationship to it and no class names anywhere changed.
Re-executing the module redefines the components declared in it. When the module's export is produced by a HOC at module scope (e.g. Sentry.wrap(...), which is common in a root _layout.tsx), the exported type is one React has not seen before, so React unmounts and remounts that component and everything below it — losing all of its state.
This is not CSS regeneration. The reproduction contains zero className usages (grep -rn className src/ returns nothing) — every style is an inline style={{ ... }}, so there is nothing to extract and the generated CSS is invariant. It is also provably unchanged: fetching the full bundle before and after editing src/unrelated.ts
curl "http://localhost:8082/node_modules/expo-router/entry.bundle?platform=ios&dev=true&hot=false&lazy=true&transform.routerRoot=src%2Fapp"
yields two bundles that differ by exactly one line — the edited constant:
< var value = 'v5'
---
> var value = 'v6'
Byte-identical uniwind output, yet the module importing it is still re-executed. The invalidation appears to be unconditional rather than driven by a change in the generated CSS. Invalidating importers only when the generated CSS actually changes (e.g. comparing an output hash) would avoid this.
Note for verification: when switching uniwind versions, restart with npx expo start --clear. A warm Metro cache masks the behaviour and made it look fixed on 1.11.0 in my first attempt. It reproduces on both 1.9.0 and 1.11.0.
Steps to Reproduce
git clone https://github.com/denniske/exporeload && cd exporeload && npm install
npx expo run:ios
- Tap Go to detail. The red overlay shows
module <id> · mounts 1.
- Edit
src/unrelated.ts — change 'v1' to 'v2' and save. That file is imported only by src/app/detail.tsx; src/app/_layout.tsx has no import path to it.
Expected: Fast Refresh updates the text to value: v2 and the overlay still reads module <same id> · mounts 1 — the root layout was not involved in the edit, so it should not be re-executed or remounted.
Actual: The text updates, but the overlay shows a different module id and mounts 2: src/app/_layout.tsx was re-executed and its component remounted.
Confirming the cause: comment out the first line of src/app/_layout.tsx (// import '../global.css';) and repeat — the module id stays stable and mounts stays at 1. Restore the import and the re-execution returns.
Instrumentation used:
MODULE_INSTANCE — a module-scope constant; a new value means the module was re-executed.
globalThis.__rootMounts — survives Fast Refresh, so it only increments on a real remount.
Snack or Repository Link
https://github.com/denniske/exporeload
Uniwind version
1.11.0 (also reproduces on 1.9.0)
React Native Version
0.86.2
Platforms
iOS (tested on the iOS Simulator, iPhone 17 / iOS 26, dev client). Not tested on Android or Web.
Expo
Yes — expo 57.0.15, expo-router 57.0.15
Additional information
The
import '../global.css';statement in a expo router layout will invalidate on any changes in the project even if no classnames are changed. In my complex project that lead to the whole app being rerendered and loosing expo router navigation state (going back to the home page). I provide a minimal example here without the navigation reset problem but demonstrating the invalidation of the global.css which I think should be fixed anyway. If the generated styles dont change it should not invalidate.Disclaimer: This reproduction and bug ticket was created with Claude Opus 5.
What happened?
Importing uniwind's CSS entry makes the importing module be re-executed on every Fast Refresh, even when the edited file has no import relationship to it and no class names anywhere changed.
Re-executing the module redefines the components declared in it. When the module's export is produced by a HOC at module scope (e.g.
Sentry.wrap(...), which is common in a root_layout.tsx), the exported type is one React has not seen before, so React unmounts and remounts that component and everything below it — losing all of its state.This is not CSS regeneration. The reproduction contains zero
classNameusages (grep -rn className src/returns nothing) — every style is an inlinestyle={{ ... }}, so there is nothing to extract and the generated CSS is invariant. It is also provably unchanged: fetching the full bundle before and after editingsrc/unrelated.tsyields two bundles that differ by exactly one line — the edited constant:
Byte-identical uniwind output, yet the module importing it is still re-executed. The invalidation appears to be unconditional rather than driven by a change in the generated CSS. Invalidating importers only when the generated CSS actually changes (e.g. comparing an output hash) would avoid this.
Note for verification: when switching uniwind versions, restart with
npx expo start --clear. A warm Metro cache masks the behaviour and made it look fixed on 1.11.0 in my first attempt. It reproduces on both 1.9.0 and 1.11.0.Steps to Reproduce
git clone https://github.com/denniske/exporeload && cd exporeload && npm installnpx expo run:iosmodule <id> · mounts 1.src/unrelated.ts— change'v1'to'v2'and save. That file is imported only bysrc/app/detail.tsx;src/app/_layout.tsxhas no import path to it.Expected: Fast Refresh updates the text to
value: v2and the overlay still readsmodule <same id> · mounts 1— the root layout was not involved in the edit, so it should not be re-executed or remounted.Actual: The text updates, but the overlay shows a different module id and
mounts 2:src/app/_layout.tsxwas re-executed and its component remounted.Confirming the cause: comment out the first line of
src/app/_layout.tsx(// import '../global.css';) and repeat — the module id stays stable andmountsstays at1. Restore the import and the re-execution returns.Instrumentation used:
MODULE_INSTANCE— a module-scope constant; a new value means the module was re-executed.globalThis.__rootMounts— survives Fast Refresh, so it only increments on a real remount.Snack or Repository Link
https://github.com/denniske/exporeload
Uniwind version
1.11.0 (also reproduces on 1.9.0)
React Native Version
0.86.2
Platforms
iOS (tested on the iOS Simulator, iPhone 17 / iOS 26, dev client). Not tested on Android or Web.
Expo
Yes — expo 57.0.15, expo-router 57.0.15
Additional information