S
SolidJS•2w ago
grobitto

Possible bug? Solidjs lazy() caches network errors

We are developing a smart tv app and as a part of checklist process there is a requirement that app should correctly handle network outages. The problem I discovered is that solid's lazy function caches TypeError: Failed to fetch dynamically imported module. I was not able to recover from this error in any way other than restart app completely - errorboundary reset not working. Steps to reproduce: switch network to offline try to load any route that uses lazy (error expected). switch network back to online lazy component error is cached and not refetched Do you think it's and error, should I report a bug? Is there any way to handle error by myself and return something that will be re-evaluated next time?
10 Replies
peerreynders
peerreynders•2w ago
If you look at the code https://github.com/solidjs/solid/blob/41fa6c14b4bf71eed593303cd63b32d53e3515e9/packages/solid/src/render/component.ts#L354-L393 you'll notice there is no failure handling code. The import function is only ever called once, so the passed function's intent is one of delayed execution, not of repetition or retrying. Lazy components by design interact with the suspense boundary but I suspect once the promise arrives at the rejected state it ends up with the next ErrorBoundary. Once there you need to restart that part of the UI with the ErrorBoundary's reset function. The reset would then have to rerun the assignment for the failed component import, something like:
// const Greeting = lazy(() => import('./greeting'));

let greetingError: Error | undefined = new Error('Load pending');
let Greeting: Component<{ name: string }>;
const resetGreeting = () => {
if (!greetingError) return;

Greeting = lazy(() => {
greetingError = undefined;
const p = import('./greeting');
p.catch((e) => (greetingError = e));
return p;
});
};
resetGreeting();
// const Greeting = lazy(() => import('./greeting'));

let greetingError: Error | undefined = new Error('Load pending');
let Greeting: Component<{ name: string }>;
const resetGreeting = () => {
if (!greetingError) return;

Greeting = lazy(() => {
greetingError = undefined;
const p = import('./greeting');
p.catch((e) => (greetingError = e));
return p;
});
};
resetGreeting();
GitHub
solid/packages/solid/src/render/component.ts at 41fa6c14b4bf71eed59...
A declarative, efficient, and flexible JavaScript library for building user interfaces. - solidjs/solid
grobitto
grobittoOP•2w ago
Looks like there is a global problem with browsers caching error response for dynamic modules: https://github.com/whatwg/html/issues/6768
GitHub
Failed dynamic import should not always be cached · Issue #6768 ·...
Given feedback from https://bugs.chromium.org/p/chromium/issues/detail?id=1195405#c9 In my opinion, HTML spec should also stop demanding to cache failed dynamic imports. The rationale of this propo...
peerreynders
peerreynders•2w ago
there is a requirement that app should correctly handle network outages.
I suspect this almost suggests using a service worker based solution (which can introduce it's own unique challenges) which pulls in (and retries) bundles pre-emptively and uses cache-busting to mitigate cached errors.
grobitto
grobittoOP•2w ago
Service workers does not work on Smart TV's. Guidelines require app to correctly present network error to user and recover after network is back, which is impossible without full page reload as dynamic import error is cached forever by browser
peerreynders
peerreynders•2w ago
Perhaps enquire in https://discord.com/channels/722131463138705510/1245822801701503026 what sort of approaches have been used to mitigate this issue. The most obvious being a single bundle on a budget.
zulu
zulu•2w ago
let greetingError: Error | undefined = new Error('Load pending');
let Greeting: Component<{ name: string }>;

let reqId=0;
let loaded = false;


const resetGreeting = () => {
if (!greetingError) return;

Greeting = lazy(() => {
greetingError = undefined;
let cacheBust = reqId > 0 && !loaded ? `?r=${reqId}` : ''
const p = import(`./greeting${cacheBust}`);
p.catch((e) => {
greetingError = e
reqId++
});
p.then(()=>loaded=true)

return p;
});
};
resetGreeting();
let greetingError: Error | undefined = new Error('Load pending');
let Greeting: Component<{ name: string }>;

let reqId=0;
let loaded = false;


const resetGreeting = () => {
if (!greetingError) return;

Greeting = lazy(() => {
greetingError = undefined;
let cacheBust = reqId > 0 && !loaded ? `?r=${reqId}` : ''
const p = import(`./greeting${cacheBust}`);
p.catch((e) => {
greetingError = e
reqId++
});
p.then(()=>loaded=true)

return p;
});
};
resetGreeting();
cache busting might work like this, the tricky part might be triggering the reset.
grobitto
grobittoOP•2w ago
Tried cache buster, but it did not work in with bundler 🙂 probably need to write some bundler plugin
zulu
zulu•2w ago
Yeah, it is trickier than I first assumed vite, does not allow import() with an expression or a variable you can use this /* @vite-ignore */ on the import to tell vite not to touch that import then make your component a manual chunk, with a predictable name.
zulu
zulu•2w ago
yeah, you might need bundler as well
No description
zulu
zulu•2w ago
good luck

Did you find this page helpful?