-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(dev): Wait for the router to be initialized in RemixBrowser before applying HMR changes #6767
fix(dev): Wait for the router to be initialized in RemixBrowser before applying HMR changes #6767
Conversation
🦋 Changeset detectedLatest commit: fe8175d The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Hi @defjosiah, Welcome, and thank you for contributing to Remix! Before we consider your pull request, we ask that you sign our Contributor License Agreement (CLA). We require this only once. You may review the CLA and sign it by adding your name to contributors.yml. Once the CLA is signed, the If you have already signed the CLA and received this response in error, or if you have any questions, please contact us at hello@remix.run. Thanks! - The Remix team |
Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳 |
I'm also running with |
I'm using the cloudflare-workers template and can confirm I'm getting this error too. @defjosiah I'm looking forward to your fix! |
aba7ca6
to
441848b
Compare
@defjosiah I have a commit over here that I'd like to see added as part of this fix: #6774 |
@jacob-ebey: sounds good! How do you want me to handle that? |
Feel free to pull it into your fork / this PR if you'd like to do that! |
…xtra pending HMR work
441848b
to
59552f4
Compare
@jacob-ebey: done! Thanks for the extra error handling |
Do I have to do anything else here, or will y'all handle merging it, etc.? |
We'll take care of it. Thanks for the contribution! 🎉 |
@@ -180,7 +202,7 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { | |||
window.__remixContext.future.v2_normalizeFormMethod, | |||
}, | |||
}); | |||
|
|||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jacob-ebey this looks like unintentional whitespace
let hmrRouterReadyResolve: ((router: Router) => void) | undefined; | ||
// There's a race condition with HMR where the remix:manifest is signaled before | ||
// the router is assigned in the RemixBrowser component. This promise gates the | ||
// HMR handler until the router is ready | ||
let hmrRouterReadyPromise = new Promise<Router>((resolve) => { | ||
// body of a promise is executed immediately, so this can be resolved outside | ||
// of the promise body | ||
hmrRouterReadyResolve = resolve; | ||
}).catch(() => { | ||
// This is a noop catch handler to avoid unhandled promise rejection warnings | ||
// in the console. The promise is never rejected. | ||
return undefined; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could use a Channel
here instead so you don't have to manually set resolve
and check for it later to make TS happy:
let hmrRouterReadyResolve: ((router: Router) => void) | undefined; | |
// There's a race condition with HMR where the remix:manifest is signaled before | |
// the router is assigned in the RemixBrowser component. This promise gates the | |
// HMR handler until the router is ready | |
let hmrRouterReadyPromise = new Promise<Router>((resolve) => { | |
// body of a promise is executed immediately, so this can be resolved outside | |
// of the promise body | |
hmrRouterReadyResolve = resolve; | |
}).catch(() => { | |
// This is a noop catch handler to avoid unhandled promise rejection warnings | |
// in the console. The promise is never rejected. | |
return undefined; | |
}); | |
let routerReady = Channel.create<Router>() | |
// ... | |
let checkRouter = await routerReady.result() | |
if (!checkRouter.ok) { | |
console.error("...") | |
return | |
} | |
let router = checkRouter.value | |
// ... | |
routerReady.ok(router) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fine with me, however, it looks like that Channel implementation is in remix-dev package?
remix/packages/remix-dev/channel.ts
Line 28 in 0d22ee9
}; |
And it doesn't look like there's an import in this react package, so not sure how y'all would want to do that.
Also, typescript seems happy with it for me! I'm getting all the proper types out of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yea good point. Not worth introducing the channel abstraction just for this if its not already in this package.
For TS, I just mean that if (hmrRouterReadyResolve) {
is unnecessary with channel, but needed here since TS doesn't know that hmrRouterReadyResolve
is guaranteed to be set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, got it!
Yeah, I really like that Channel abstraction, probably going to shamelessly use it somewhere else 😂
Hi, should I be keeping this up to date? |
@defjosiah I'll review this with @jacob-ebey on Monday |
🤖 Hello there, We just published version Thanks! |
🤖 Hello there, We just published version Thanks! |
🤖 Hello there, We just published version Thanks! |
Fixes: #6415
Description
With the cloudflare-workers template (and likely pages), there is a race condition. Sometimes the
router
that is used inthe
import.meta
setup in theRemixBrowser
component file is undefined.My guess is that the hmr update is sent before the component is hydrated (which is where the file global
let router: Router variable is assigned).
This adds a guard to the
import.meta.hot.accept
function, which waits for the router to be initialized before it is used inthe rest of the function.
Testing Strategy
I discovered this in devtools, I have a reproducing example locally (same as the issue this closes).
Without my change, you'll see that the router is undefined, and the error:
is shown. There is also no HMR that works.
With my change, built and applied locally, it properly does HMR and has no errors:
I looked for places to add tests for this, but I don't see any component level tests for the component, and it looked like a large can of worms to attempt to add the first tests for this component. The hmr integration test will catch if this is an issue with the existing hmr setup.
Reproduction Steps
Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: http://localhost:8787/build/entry.client-EGKEKBAB.js
)