Skip to content

feat: add onError and onLoad lifecycle hooks with setConfig#1037

Open
roidayan1 wants to merge 1 commit into
gregberge:mainfrom
roidayan1:feat/on-error-on-load-hooks
Open

feat: add onError and onLoad lifecycle hooks with setConfig#1037
roidayan1 wants to merge 1 commit into
gregberge:mainfrom
roidayan1:feat/on-error-on-load-hooks

Conversation

@roidayan1

@roidayan1 roidayan1 commented Jul 9, 2026

Copy link
Copy Markdown

Related: #667 — that issue asked for a built-in retry policy ({ retries }) and was closed as wontfix. This PR takes the lighter, unopinionated route: a general onError hook (mechanism, not policy) that lets consumers implement retry / reload / reporting themselves, plus a symmetric onLoad.

Summary

loadable already has an internal onLoad, but there is no supported way for a consumer to react to a component load — success or failure. Today a failed dynamic import() can only be handled by an Error Boundary, and there is no success callback at all.

This PR adds two small, opt-in, backward-compatible lifecycle hooks:

  • onError(error, props) — called when a dynamic import fails. It is a notification hook: the error is still thrown afterwards, so your Error Boundary keeps working as the last line of defense. Use it to report the failure or to recover (e.g. reload the page after a deploy invalidated the chunk hashes).
  • onLoad(result, props) — called when a dynamic import resolves.

Both can be set:

// per-instance
loadable(() => import('./X'), {
  onError: (error, props) => reportError(error),
  onLoad:  (result, props) => {},
})

// globally
loadable.setConfig({ onError, onLoad })   // instance option wins over global

lazy and the .lib variants inherit them (same createLoadable).

Why

Reacting to lazy-load failures is a common need (the classic "Failed to fetch dynamically imported module" after a deploy). Wrapping every loadable() call by hand is repetitive, and there is no global entry point. A first-class hook + a global setConfig default covers both, while staying a mechanism (recovery policy is left to the consumer) — see docs for a reload-once-with-guard example.

Notes

  • Backward compatible: no onError/onLoad → behavior is byte-for-byte unchanged (error still thrown to the Error Boundary).
  • Fires once per load, including in suspense mode where the render path can attach several then/catch handlers to the same cached promise (deduped per instance).
  • New tiny modules: config.js (global store) and hooks.js (instance-over-global resolver). setConfig is also exported as a named export.

Tests & docs

  • Tests added to packages/component/src/loadable.test.js (#onError, #onLoad): args, precedence (instance over global), fires-once-in-suspense, and that onError still reaches the Error Boundary.
  • Docs updated: api-loadable-component (options tables + onError/onLoad/setConfig sections) and error-boundaries (a stale-chunk reload-once recovery example).

loadable had an internal onLoad but no way for consumers to react to a
component load. This adds two symmetric, opt-in lifecycle hooks:

- `onError(error, props)` — called when a dynamic import fails. It is a
  notification hook; the error is still thrown so an Error Boundary can catch
  it. Recovery (reload, report, ...) is left to the consumer.
- `onLoad(result, props)` — called when a dynamic import resolves.

Both are available per-instance via `loadable(fn, { onError, onLoad })` and
globally via `loadable.setConfig({ onError, onLoad })`; a per-instance option
takes precedence over the global default. `lazy` and the `.lib` variants get
them too.

Hooks fire once per load even in suspense mode, where the render path can
attach several then/catch handlers to the same cached promise.

Docs and tests included.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@roidayan1

Copy link
Copy Markdown
Author

@theKashey when you have a moment, would you be open to reviewing this? It's small, additive and backward-compatible: two opt-in lifecycle hooks (onError / onLoad) with per-instance and global (setConfig) forms, plus tests and docs. Happy to adjust the API shape or naming to fit your preferences. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant