react-refresh-webpack-plugin icon indicating copy to clipboard operation
react-refresh-webpack-plugin copied to clipboard

Webpack top level await doesn't work correctly

Open tqnghia1998 opened this issue 2 years ago • 1 comments

Hello pmmmwh, I found out that your lib doesn't fully support TLA, let me give an example:

We have 3 files in the same folder: App.jsx, A.jsx and B.jsx

App.jsx

import A from './A'
import B from './B'

export default function App() {
   return <div>App</div>
}

A.jsx

await new Promise((resolve) => setTimeout(() => resolve(0), 1000))

B.jsx

await new Promise((resolve) => setTimeout(() => resolve(0), 2000))

The flow of setup & cleanup is (I printed it out):

  1. setup(App): $Refresh$.moduleId = App, $Refresh$.cleanup = cleanup_of_App

  2. setup(A): $Refresh$.moduleId = A, $Refresh$.cleanup = cleanup_of_A Because A is still loading, so it's not finished yet, meanwhile, start loading B (it's how webpack works)

  3. setup(B): $Refresh$.moduleId = B, $Refresh$.cleanup = cleanup_of_B Because B is still loading, so it's not finished yet, meanwhile, A is finished

  4. cleanup(A): Calling $Refresh$.cleanup (= cleanup_of_B), inside the function, currentModuleId !== cleanupModuleId so the cleanup don't happen, $Refresh$ don't change. Furthermore, when the code runs into getRefreshModuleRuntime block, $ReactRefreshModuleId$ gets wrong value

const $ReactRefreshModuleId$ = __webpack_require__.$Refresh$.moduleId; // In A.jsx, the value is B.jsx
const $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(
    $ReactRefreshModuleId$
);

And because the cleanup don't happen, it trigger a domino effect, leads to every file has wrong $ReactRefreshModuleId$ value, leads to the HMR don't work properly


About the timeout number

The reason I set timeout of B (2000ms) higher than A (1000ms) is that I want A to be finished before B (while the current $Refresh$ is still storing values of B). If I set timeout of B smaller than A, everything works normally.


The root cause

The root cause of this issue is the way webpack handle multiple imported async modules. Let's say, if App.jsx import 3 async modules A.jsx, B.jsx and C.jsx in the order A > B > C, the order when they are resolved can be randomly (A > C > B, B > C > A, etc). Therefore leads to mismatch __webpack_require__.$Refresh$.moduleId


My suggest solution

Let's go back to the code of getRefreshModuleRuntime block, can we change

const $ReactRefreshModuleId$ = __webpack_require__.$Refresh$.moduleId;

into

const $ReactRefreshModuleId$ = module.id;

That will ensure the correct module id always. What I don't understand is why you don't use it in the first place, is there any case that __webpack_require__.$Refresh$.moduleId should be different from module.id?

Thank you @pmmmwh and others, I attach a working example below, please give it a try

webpack-tla-not-work.zip

tqnghia1998 avatar Feb 20 '23 03:02 tqnghia1998

Thanks for submitting this! I've been seeing similar behavior when webpack has asyncWebAssembly enabled and modules import a library with async wasm (e.g. rapier). Refreshes no-op with no explanation in the console.

The workaround was to avoid this type of import dependency for now.

chromakode avatar Mar 24 '24 18:03 chromakode