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 because lazy
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
.
React.lazy
accepts any async function that returns a module, so you could do something like this -
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 lazy
implementation is the scenario where we want to retry loading a previously failed async module load after the initial attempt(s) gave up.
Imagine an app code-split by route:
Since lazy
is caching a failed promise, we can't do the 2nd part of 2 in a timely fashion or do 4 at all right now. Changing lazy
to cache only fulfilled promises and forget rejected ones would allow us to support this use case, which seems something we would like React to facilitate.
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 React.lazy
component whenever the import promise rejects, and assigns it back to the component variable:
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 Other
and keep using it forever):
<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 lazy
can make this common scenario very simple.
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.
The proposed workarounds aren’t ideal, but if they unblock you and you’re ok with the perf after measuring it, might be ok for now. Will report back on this issue when we have more news.
@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 React.lazy
a few days ago, might help: https://github.com/facebook/react/pull/14626.
Any update on this Issue ???
Rejected and cached the failed result. next time its not triggering the fresh call to retrieve the data
I have made a pull request #15296 and added a test case. Please review and comment.
Thanks.
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 importWithRetries
utility where you can pass the number of retries and the path of the file.
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.
Also it looks like everybody tries to do automatic retries and what about showing user some message and for example retrying on user action like clicking retry button?
Edit:
I have found a way how to handle errors with retrying on user interaction if anyone needs that:
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.
Most helpful comment
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
React.lazy
component whenever the import promise rejects, and assigns it back to the component variable: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
Other
and keep using it forever):Hope this helps at least make it work while this is solved!