-
Notifications
You must be signed in to change notification settings - Fork 48.5k
React.lazy does not allow retrying a rejected promise #14254
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
Comments
const Something = React.lazy(async () => {
let mod;
while(!mod){
try{
mod = await import('./something')
}
catch(err){}
}
// this does infinite retries,
// you could modify it to do it only thrice,
// or add backoff/network detection logic, etc
return mod
}) (I haven't tested the above, but I think it should work?) |
@threepointone that would immediately retry loading a failed module until it eventually fails and gives up, which could address part of the problem. The part we can't currently address unless we change Imagine an app code-split by route:
Since This is applicable to any lazily loaded component, not only at "route-based" split points. |
FWIW, I ran into this same problem, and found this issue while doing research as to who was caching the promise (thanks for filing it!). I found a workaround if you still have to make it work until this is properly solved here. Codesandbox is here: https://codesandbox.io/s/1rvm45z3j3 Basically I created this function, which creates a new let Other;
function retryableLazy(lazyImport, setComponent) {
setComponent(
React.lazy(() =>
lazyImport().catch(err => {
retryableLazy(lazyImport, setComponent);
throw err;
})
)
);
}
retryableLazy(
() => import("./Other"),
comp => {
Other = comp;
}
); Another important aspect is that the error boundary of your component should be using something like render props, in order to use the new value that the variable references at any point in time (otherwise it will always use the first value assigned to <ErrorBoundary>
{() => (
<Suspense fallback={<div>Loading...</div>}>
<Other />
</Suspense>
)}
</ErrorBoundary> Hope this helps at least make it work while this is solved! |
Thanks for sharing @leoasis. There are workarounds for this in user-land, albeit rather convoluted. Changing Is there any known drawback to this proposed change, like for example affecting performance in a negative way? |
We definitely want to support this in a first class manner from react, but I don’t have any specific details to share yet. |
@threepointone is there any updates about this issue? |
If there was an update — it would be on this issue. :-) You can help drive it by submitting a failing test case, with or without a fix. Here's an example of me changing something in |
Any update on this Issue ??? |
I have made a pull request #15296 and added a test case. Please review and comment. |
This could be done at the promise level retrying with a catch handler provided to the promise returned by the call to import, and you can even create an |
Handling lazy loading was so much easier with use of react-loadable (unfortunately it doesn't look like its maintained) it shame that its done so poorly in main library. Edit: import * as React from 'react';
export default function LazyLoaderFactory<T = any>(
promise: () => Promise<{ default: React.ComponentType<any> }>,
Loader = () => <>Loading...</>,
ErrorRetryView = ({ retry }) => <button onClick={retry}>Module loading error. Try again.</button>,
) {
function LazyLoader(props: T) {
const [ loading, setLoading ] = React.useState<boolean>(true);
const retry = React.useCallback(() => setLoading(true), []);
const Lazy = React.useMemo(() => React.lazy(() => promise().catch(() => {
setLoading(false);
return { default: () => <ErrorRetryView retry={retry}/> };
})), [promise, loading]);
return <React.Suspense fallback={<Loader/>}><Lazy {...props}/></React.Suspense>;
}
(LazyLoader as any).displayName = `LazyLoader`;
return LazyLoader as React.ComponentType<T>;
} |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution. |
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you! |
This issue (feature request) is still valid. I think that retrying should have first-class support. |
Yes, this issue isn't fixed. What's wrong with different caching behaviour based on whether the Promise fulfilled or rejected? |
For anyone else coming across this issue; I've gone for a slightly different workaround: <ErrorBoundary>
{() => (
<Suspense fallback={<div>Loading component...</div>}>
{React.createElement(
React.lazy(() => import("./my/Component")),
{ aProp: "aValue" }
)}
</Suspense>
)}
</ErrorBoundary>; This is re-rendered by a retry button in the ErrorBoundary that changes the ErrorBoundary state. |
Sorry for bringing back an old post, but I think I've found a way to reproduce this. In my casse, this error only happens when redeploying. In my case i'm using Vite to bundle a React SPA with React Router and a global app-level error boundary. Say you have a user browsing your deployed app, and you deploy a new version that changes something in a component that is being dynamically loaded. Well, when the deploy finishes, your old version of the dynamically loaded modules would no longer be available because of cache busting, but the new ones will be. So when the user, that had the tab open with the same url all this time goes to browse another route, the module loading system will crash. I implemented a very very hacky workaround for this like this (TS Code): const MODULE_IMPORT_ERROR = 'Failed to fetch dynamically imported module'
function handleError(error: Error) {
// eslint-disable-next-line
console.error(error)
// if the error starts with this message, reload the whole app
if (error.message.indexOf(MODULE_IMPORT_ERROR) === 0) {
window.location.reload()
}
}
export default function ErrorCatcher({ children }: React.PropsWithChildren<unknown>) {
return (
<ErrorBoundary FallbackComponent={ErrorMessage} onError={handleError}>
{children}
</ErrorBoundary>
)
} |
Do you want to request a feature or report a bug?
It can be seen as a feature or a bug, depending on angle. Let's say it's an enhancement to how
lazy
works.What is the current behavior?
When using
React.lazy
, if the given promise rejects while trying to asynchronously load a component, it's no longer possible to retry loading the component chunk becauselazy
internally caches the promise rejection.If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:
This does not seem to work great in CodeSandbox because it's using service workers, which get in the way when simulating offline mode, yet this small app illustrates the issue: https://codesandbox.io/s/v8921j642l
What is the expected behavior?
A promise rejection should not be cached by
lazy
and another attempt to render the component should call the function again, giving it the chance to return a new promise.Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
AFAIK all version of React that include
lazy
.The text was updated successfully, but these errors were encountered: