loadable-components icon indicating copy to clipboard operation
loadable-components copied to clipboard

Implement retry in case of error

Open ValentinH opened this issue 5 years ago • 32 comments

🚀 Feature Proposal

Implement a new "retry" option in @loadable/component.

Motivation

We monitor our errors with Sentry and we see quite a lot of errors coming with lazy-loaded components. Indeed, on flaky network, it's easy to have a request aborted and the component is never loaded. In our applications, we have started to wrap all dynamic imports passed to loadable with a retry function such as:

const Example = loadable(() =>
  retry(
    () => import('./Example'),
    { retries: 3 }
  )
)

where retry is a simple function:

type Options = {
  retries?: number
  interval?: number
  exponentialBackoff?: boolean
}

// taken from https://dev.to/goenning/how-to-retry-when-react-lazy-fails-mb5
export function retry<R>(
  fn: () => Promise<R>,
  { retries = 3, interval = 500, exponentialBackoff = true }: Options = {}
) {
  return new Promise<R>((resolve, reject) => {
    fn()
      .then(resolve)
      .catch((error) => {
        setTimeout(() => {
          if (retries === 1) {
            reject(error)
            return
          }

          // Passing on "reject" is the important part
          retry(fn, {
            retries: retries - 1,
            interval: exponentialBackoff ? interval * 2 : interval,
          }).then(resolve, reject)
        }, interval)
      })
  })
}

Example

const Example = loadable(() => import('./Example'),  {retries: 3})

Pitch

I'm wondering if this is something that could be directly implemented as part of loadable.

ValentinH avatar Dec 07 '20 13:12 ValentinH

Good point. As a matter of fact - our own Sentry is filled with the same reports.

theKashey avatar Dec 07 '20 22:12 theKashey

If you agree, I'd be happy to work a PR.

ValentinH avatar Dec 07 '20 22:12 ValentinH

That could be a little complicated as right now errors are just thrown, expected to be handled on the nearest ErrorBoundary. The question - should network problems be still reported (so the error has to be thrown), or in case they are retry-able loadable can swallow them?

theKashey avatar Dec 07 '20 23:12 theKashey

I don't know if you are able to differentiate network errors from other kinds of errors when using dynamic imports. Do you know?

Regarding errors just being thrown at the moment, if the retry is optional, we could still retry all errors as it would not be a breaking change. What do you think?

ValentinH avatar Dec 08 '20 07:12 ValentinH

I don't know if you are able to differentiate network errors

You can, but that is abstraction leaking, so you should not try to differentiate.

Regarding errors just being thrown at the moment...

This moment can alter the implementation a lot. Providing ErrorBoundary with built-in retry mechanism and built-in reporting can be a better solution, and actually can be done right now. Well, almost - such error boundary cannot distinguish LoadableError from any error coming from within loaded content. So we are circling back to the problem of "how to differentiate"

theKashey avatar Dec 09 '20 04:12 theKashey

I might be missing something but I think the retry function I shared in my issue is only retrying errors related to the dynamic import, not errors within the loaded component.

By the way, I deployed it on our main app in production and almost all the ChunkLoadError are gone.

ValentinH avatar Dec 09 '20 06:12 ValentinH

If only error boundary could detect chunk loading error (thrown by loadable) - then it can do retry.

Wrapping import with any extra code is breaking SSR. There is an update to a babel plugin to make that impossible as well.

theKashey avatar Dec 09 '20 08:12 theKashey

Wrapping import with any extra code is breaking SSR. There is an update to a babel plugin to make that impossible as well.

What do you mean by "breaking SSR"? We use this technique on an SSR application and I didn't notice any issues. 🤔

ValentinH avatar Dec 09 '20 10:12 ValentinH

Hello! Facing the same issue from some time. I would like to try @ValentinH solution, will let you know when it will be tested it on production in my project

sosnomIntent avatar Dec 10 '20 12:12 sosnomIntent

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 contributions.

stale[bot] avatar Feb 09 '21 03:02 stale[bot]

I think this issue is still relevant. My last question was not answered actually.

ValentinH avatar Feb 09 '21 06:02 ValentinH

What do you mean by "breaking SSR"? We use this technique on an SSR application and I didn't notice any issues. 🤔

Loadable babel plugin is reading your imports and create another structure which is expected to "do the same". But logic there is very simply - it reads a file from the import and rewrites it to require for SSR. In other words - all your "additions" will be just discarded.

See test - https://github.com/gregberge/loadable-components/blob/main/packages/babel-plugin/src/snapshots/index.test.js.snap#L27-L56

theKashey avatar Feb 09 '21 23:02 theKashey

Actually, I'm fine if my addition are discarded for the server bundle. The important thing here is to retry on the browser one. Or do you mean that it will actually break the require on the server?

I checked and on our applications we actually only use loadable in components that are wrapped with NoSsr wrappers.

ValentinH avatar Feb 10 '21 07:02 ValentinH

It's more about the general unpredictibility of the result. Let's double-check if #511 can solve your issue. Probably with the minimal changes we can teach guard to retry import if needed. cc @hedgepigdaniel

theKashey avatar Feb 11 '21 22:02 theKashey

It's more about the general unpredictibility of the result. Let's double-check if #511 can solve your issue. Probably with the minimal changes we can teach guard to retry import if needed. cc @hedgepigdaniel

Hmm maybe - perhaps if guard received a createImportPromise/tryImport (that could be called multiple times to retry) rather than an already created importPromise

hedgepigdaniel avatar Mar 01 '21 03:03 hedgepigdaniel

Sounds correct. However, before doing anything we should double-check our ability to measure and report such problems.

So, @ValentinH - how did you found that you have this problem? NewRelic, Sentry, other custom error monitoring? Is there something we can do better here?

theKashey avatar Mar 01 '21 06:03 theKashey

I found this issue thanks to our Sentry integration. If I recall correctly the error was "Chunkload error". When we started wrapping all our dynamic imports with the retry function I shared in the original post, we noticed a clear decrease of that error.

ValentinH avatar Mar 01 '21 07:03 ValentinH

hi guys..any update?

Etto91 avatar Mar 30 '21 09:03 Etto91

@hedgepigdaniel is busy right now a little but I hope quite soon we'll break the status quo.

theKashey avatar Mar 30 '21 10:03 theKashey

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 contributions.

stale[bot] avatar Jun 02 '21 15:06 stale[bot]

This issue is still relevant.

ValentinH avatar Jun 02 '21 17:06 ValentinH

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 contributions.

stale[bot] avatar Aug 02 '21 02:08 stale[bot]

This issue is still relevant.

ValentinH avatar Aug 02 '21 04:08 ValentinH

Any update on this topic, I think it's still relevant what @ValentinH suggested!

kamalbennani avatar Sep 17 '21 07:09 kamalbennani

I'm currently trying to use the retry function created by @ValentinH to wrap my import statements as follows:

const SomeComponent = loadable(() => retry(() => import('./SomeComponent')))

The code works great (thank you!) but unfortunately it masks the types of the props of SomeComponent. I've tried to import types from loadable__types but haven't found a way to pass the props from retry to loadable.

For instance, this attempt:

import { DefaultComponent } from 'loadable__component'

type RetryType = () => Promise<{ default: React.ComponentType<any> }>

export function retry(
  fn: () => Promise<DefaultComponent<RetryType>>,
  { retries = 3, interval = 500, exponentialBackoff = true }: Options = {}
) {
  return new Promise<<DefaultComponent<RetryType>>((resolve, reject) => {
    fn()
      .then(resolve)
      .catch((error) => {
        setTimeout(() => {
          if (retries === 1) {
            reject(error)
            return
          }

          // Passing on "reject" is the important part
          retry(fn, {
            retries: retries - 1,
            interval: exponentialBackoff ? interval * 2 : interval,
          }).then(resolve, reject)
        }, interval)
      })
  })
}

...gives this error:

...Type 'Promise<typeof import("path/to/file")>' is not assignable to type 'Promise<DefaultComponent<RetryType>>'.

Any suggestions on improving the type of the retry function?

YPCrumble avatar Sep 28 '21 16:09 YPCrumble

@YPCrumble you need to use a generic signature. I have updated my original message with what you are looking for 😉

ValentinH avatar Sep 28 '21 17:09 ValentinH

@ValentinH worked perfectly, thank you @ValentinH !!!

YPCrumble avatar Sep 28 '21 18:09 YPCrumble

This issue is still problematic, if it's not prioritized / fixable at the moment can we link this in the docs? Discoverability of this solution can help future users from wasting few hours hopefully.

divyanshu013 avatar Nov 17 '21 18:11 divyanshu013

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 contributions.

stale[bot] avatar Apr 16 '22 05:04 stale[bot]

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 contributions.

stale[bot] avatar Jun 19 '22 05:06 stale[bot]