Skip to content

global.css import invalidates the importing module on every unrelated edit (Fast Refresh remounts the root layout) #641

Description

@denniske

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

  1. git clone https://github.com/denniske/exporeload && cd exporeload && npm install
  2. npx expo run:ios
  3. Tap Go to detail. The red overlay shows module <id> · mounts 1.
  4. 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

  • I've searched for similar issues in this repository and found none

Metadata

Metadata

Assignees

No one assigned

    Labels

    invalidThis doesn't seem right

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions