T
TanStack8mo ago
genetic-orange

How to catch chunk loading issues and reload gracefully?

In Vite, if a lazy chunk fails to be loaded, an event is dispatched to the window: 'vite:preloadError'. For reference, I'll just copy the explanation from the vite docs:
When a new deployment occurs, the hosting service may delete the assets from previous deployments. As a result, a user who visited your site before the new deployment might encounter an import error.
https://vite.dev/guide/build.html#load-error-handling When using tanstack/router, the loading of route chunks seems to be handled by tanstack instead of Vite and this event is not dispatched. (Tell me if I'm wrong here, but I couldn't catch it). The usual solution for this kind of issue is to do a "real" navigation (full page load) instead of a client-side navigation, which should pretty much be transparent for the user. However with tanstack/router, it seems that we first display the errorComponent first, and then reload the page. This leads to a bit of content flashing, and an error sent to Sentry. How would you handle this gracefully? Since all our navigations are preloaded, I was thinking of adding something like this to __root:
beforeLoad: ({ preload }) => {
if (!preload && window.failedChunk) {
window.location.reload()
return new Promise(() => {})
}
},
onEnter: () => {
window.addEventListener('vite:preloadError', (event) => {
event.preventDefault()
window.failedChunk = true
})
},
beforeLoad: ({ preload }) => {
if (!preload && window.failedChunk) {
window.location.reload()
return new Promise(() => {})
}
},
onEnter: () => {
window.addEventListener('vite:preloadError', (event) => {
event.preventDefault()
window.failedChunk = true
})
},
But this doesn't work since this event doesn't seem to be thrown when chunks are loaded by tanstack/router. Alternatively, maybe this, to at least avoid flashing the error component?
errorComponent: ({ error }) => {
if ([
'Failed to fetch dynamically imported module', // chrome
'error loading dynamically imported module', // firefox
'Importing a module script failed', // safari
].includes(error.message)) return null

// regular error component omitted for brevity
// ...
}
errorComponent: ({ error }) => {
if ([
'Failed to fetch dynamically imported module', // chrome
'error loading dynamically imported module', // firefox
'Importing a module script failed', // safari
].includes(error.message)) return null

// regular error component omitted for brevity
// ...
}
But this seems a little unwieldy and indirect. Any better ideas?
46 Replies
optimistic-gold
optimistic-gold8mo ago
we already have this implemented
optimistic-gold
optimistic-gold8mo ago
GitHub
router/packages/react-router/src/lazyRouteComponent.tsx at main · T...
🤖 Fully typesafe Router for React (and friends) w/ built-in caching, 1st class search-param APIs, client-side cache integration and isomorphic rendering. - TanStack/router
genetic-orange
genetic-orangeOP8mo ago
Weird I tested and saw a flash of the errorComponent before the reload. I'll try a repro and keep you posted (or probably not because you're probably right and I won't be able to repro)
optimistic-gold
optimistic-gold8mo ago
should not bring up the error component
optimistic-gold
optimistic-gold8mo ago
GitHub
router/packages/react-router/src/lazyRouteComponent.tsx at main · T...
🤖 Fully typesafe Router for React (and friends) w/ built-in caching, 1st class search-param APIs, client-side cache integration and isomorphic rendering. - TanStack/router
genetic-orange
genetic-orangeOP8mo ago
I think that's still the correct one (or I'd have 2000 sentry issues spamming me right now lol) I'm curious though that in the Lazy component in order to "Return empty component while we wait for window to reload" there is this:
return {
default: () => null,
}
return {
default: () => null,
}
https://github.com/TanStack/router/blob/2466ec190cb80c7e702330b0e953268586b5b3fb/packages/react-router/src/lazyRouteComponent.tsx#L94-L96 Shouldn't it be return null? Or is this some react magic I don't know?
optimistic-gold
optimistic-gold8mo ago
that's a fake "module" with a default export
genetic-orange
genetic-orangeOP8mo ago
but just below, it actually returns an actual element in the same function, right?
const lazyComp = function Lazy(props: any) {
if (error)
return {
default: () => null,
}
}
return React.createElement(comp, props)
}
const lazyComp = function Lazy(props: any) {
if (error)
return {
default: () => null,
}
}
return React.createElement(comp, props)
}
optimistic-gold
optimistic-gold8mo ago
you might be right. if you can, dive into it and see what happens if you return just () => null
genetic-orange
genetic-orangeOP8mo ago
yeah I haven't ran the package locally yet, but I might give it a go. In the meantime, I think the error message you pointed out is only valid for Chrome. I just whipped up a quick demo and opened it on chrome / firefox / safari https://stackblitz.com/edit/vitejs-vite-ztyqvabt?file=src%2FApp.tsx,package.json&terminal=dev and got 3 different messages
// chrome: Failed to fetch dynamically imported module: blob:foobar
// firefox: error loading dynamically imported module
// safari: Importing a module script failed.
// chrome: Failed to fetch dynamically imported module: blob:foobar
// firefox: error loading dynamically imported module
// safari: Importing a module script failed.
optimistic-gold
optimistic-gold8mo ago
oh no we need a robust solution then
genetic-orange
genetic-orangeOP8mo ago
Ok i tried something, but there's one thing left I'm not sure how to fix https://github.com/TanStack/router/pull/3262 (see issue 4 in the PR description)
GitHub
fix(lazyRouteComponent): improve error handling for module loading ...
Issues currently on main for lazyRouteComponent: The placeholder ReactNode used "while we wait for the page to reload" is not a valid react node return { default: () =>...
genetic-orange
genetic-orangeOP8mo ago
With this PR, nothing flashes, if a "module loading error" occurs, we remain in the "navigation loading state" until the page reloads
optimistic-gold
optimistic-gold8mo ago
nice! w.r.t. 4 we can easily solve this with autocode splitting the code generator can just insert the filename there to have a unique id
genetic-orange
genetic-orangeOP8mo ago
that's nice if we have the hashed file name at build time easily, then yeah that's definitely the way to go
optimistic-gold
optimistic-gold8mo ago
does it have to be the hashed one?
genetic-orange
genetic-orangeOP8mo ago
i think so yeah
optimistic-gold
optimistic-gold8mo ago
oh well. we don't have this before building we would need to hash ourselves than. based on time?
genetic-orange
genetic-orangeOP8mo ago
ah ah yeah that was what I thought too time isn't ideal, it would be similar to a random number basically
optimistic-gold
optimistic-gold8mo ago
a lot of all this is not ideal really...
genetic-orange
genetic-orangeOP8mo ago
I'm just talking "best case scenario" here, but in theory Vite doesn't have to re-build everything on every build, and so it would be nice to preserve the hashes for files that aren't re-built
optimistic-gold
optimistic-gold8mo ago
parsing error messages that are browser dependent
genetic-orange
genetic-orangeOP8mo ago
(even though in practice unless you do some heavy manual chunking, basically every file is going to be re-hashed) i mean, they are re-built, but the hash could be the same if the content of the file hasn't changed, but in practice it almost always does because rolldown does a lot of circular imports to optimize for "initial page load" over "recurring cached visits"
optimistic-gold
optimistic-gold8mo ago
can we hash the file content? btw we do not only support vite/rolldown, so this has to work with rspack etc as well
genetic-orange
genetic-orangeOP8mo ago
I guess... I'm not sure what the algorithm is, because when the hash of 1 file changes, the import of that file changes in every file that imports it, and it cascades. So I'm not sure when in the process you could hash the file and get a correct result
optimistic-gold
optimistic-gold8mo ago
why would it matter though? if the file contents of the to be imported file changes, a new file will be built eventually (Whatever it will be bundled with)
genetic-orange
genetic-orangeOP8mo ago
the current situation kinda works: only if a user session on safari and spans 2 deploys, then you don't get the automatic reload
optimistic-gold
optimistic-gold8mo ago
yes, but cant this be fixed with a hash over the to be imported file? or would that not help if the transitive imports change but the file itself does not?
genetic-orange
genetic-orangeOP8mo ago
yeah but if you hash before the imports are changed to point to the new files, then you could end up with a hash that hasn't changed, even though in reality it's a new file yeah exactly, it's a transitive imports thing there is probably somewhere we could plug this in the plugin, i just don't know when... but at some point Vite (or rspack or whatever) will replace the import(foo) with import(foo-4567HJH.js) and it's at that moment that we'd want to set / modify the ID
optimistic-gold
optimistic-gold8mo ago
I only can think of messy hacky things here
genetic-orange
genetic-orangeOP8mo ago
I've never used unplugin so i don't know if there is a clean solution
optimistic-gold
optimistic-gold8mo ago
the problem I see is that the bundle generation takes place pretty far down the line
genetic-orange
genetic-orangeOP8mo ago
yep
optimistic-gold
optimistic-gold8mo ago
at that time I dont think you should mess with the code
genetic-orange
genetic-orangeOP8mo ago
maybe... just the file name (without hash) would be a good fallback for safari (and keep using the error message for the 2 other browsers)
optimistic-gold
optimistic-gold8mo ago
but how would that help with the two deployments thing? would only help if you happen to fail to load two different files subsequently?
genetic-orange
genetic-orangeOP8mo ago
that way on safari, you get autoreload once per "file" (pre-chunking) per session, instead of just once per session at all if you hit the issue across 2 deployments on the same file, then it doesn't help but if you hit the issue on 2 different files then it does help
optimistic-gold
optimistic-gold8mo ago
we could use the build manifest
genetic-orange
genetic-orangeOP8mo ago
you mean to load the manifest client-side? Or to modify the emitted chunks post build?
optimistic-gold
optimistic-gold8mo ago
load client side in case of a module load error split off in a separate bundle so it would be zero-cost when no error occurs funnily (?) we already use the manifest for start... but we only load it on the server (partially)
genetic-orange
genetic-orangeOP8mo ago
I've never played with the manifest. Is it common for it to have a static name? (If name is hashed, we have the same issue lol)
optimistic-gold
optimistic-gold8mo ago
no the manifest name can be whatever you want by grabbing the contents in generateBundle either that, or it will be manifest.json
genetic-orange
genetic-orangeOP8mo ago
it feels like this would work, but i find this solution a little "itchy", to load something to know if a failed load should cause a reload
optimistic-gold
optimistic-gold8mo ago
you are right didnt think about it it would also fail unless we include it in the mainbundle which is not ideal how about we merge as is, you create a new issue and copy over the 4. point so we are aware of a potential safari issue?
genetic-orange
genetic-orangeOP8mo ago
that works for me
genetic-orange
genetic-orangeOP8mo ago
I finally created the issue, sorry for the delay https://github.com/TanStack/router/issues/3327
GitHub
Auto-reload on failed lazy route loading can only work once per ses...
Which project does this relate to? Router Describe the bug This issue is a follow-up of #3262. The 4th issue mentioned in this PR hasn't been solved yet. Basically: the system to auto-reload th...

Did you find this page helpful?