feat: drop React 18 support and migrate to React 19 idioms#796
Conversation
- Narrow react/react-dom peer deps from >=18 to >=19 (@lifi/widget, @lifi/wallet-management, @lifi/widget-light) - Remove forwardRef from 11 components (ref passed as a prop; useImperativeHandle retained) - <Context.Provider> -> <Context> and useContext -> use() across widget, wallet-management, widget-provider-*, widget-embedded - Header ResizeObserver -> React 19 ref-cleanup callback - Declare react >=19 peer dependency on the 6 widget-provider* packages
- MUI v7 -> v9, add Tron ecosystem to overview + dependency graph - Correct i18n locale count (17), note widget-light has no tests yet - Replace obsolete 'beta pre-mode' release section with the current 4.x stable line (pre-mode exited, latest on npm is 4.x)
🦋 Changeset detectedLatest commit: 28635d6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 13 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ E2E Dev Smoke — passing
4 passed · 0 failed · 0 skipped · 18s |
E2E Examples — all passedAll examples passed in the latest run. |
E2E Playground resultsDetails
📥 Download full HTML report (open the run → Artifacts → |
🔍 QA Review — EMB-456
🔁 Re-Review — Resolution Status
✅ Issue #1 —
|
There was a problem hiding this comment.
Requesting changes on 2 items — each requires either a code fix or an explicit acceptance comment with justification before this review is considered complete.
| # | Severity | Type | Issue / File |
|---|---|---|---|
| 1 | 🟠 Medium | Code | ExchangeRateBottomSheet.tsx — unsafe RefObject cast in handleContinue/handleCancel/handleClose bypasses React 19 ref type safety |
| 2 | 🟢 Low | Code | TokenDetailsSheetContent.tsx — stale typeof ref !== 'function' guard is dead code in React 19 |
1. [Medium] ExchangeRateBottomSheet.tsx — unsafe RefObject cast in handlers
handleContinue, handleCancel, and handleClose all call (ref as RefObject<ExchangeRateBottomSheetBase>).current?.close(...). The ref prop is typed Ref<ExchangeRateBottomSheetBase> which is RefObject | RefCallback | null. Casting to RefObject without narrowing silently does nothing if a callback ref or null is passed — RefCallback has no .current property. In the same file, useImperativeHandle correctly accepts ref directly, but the handlers bypass it by calling into ref.current directly instead of using the already-in-scope bottomSheetRef.current.
Additionally, handleContinue is not wrapped in useCallback while handleCancel and handleClose are — this creates an inconsistent stability profile for the three sibling handlers.
Suggested fix: Replace all (ref as RefObject<ExchangeRateBottomSheetBase>).current?.close(...) calls in the three handlers with bottomSheetRef.current?.close(...). Wrap handleContinue in useCallback([onContinue]) to match the other handlers.
File: packages/widget/src/pages/TransactionPage/ExchangeRateBottomSheet.tsx lines ~40–57.
2. [Low] TokenDetailsSheetContent.tsx — stale typeof ref !== 'function' guard
The close button handler:
if (ref && typeof ref !== 'function') {
ref.current?.close()
}This guard was required in the forwardRef era to defend against legacy callback refs. Now that ref is a plain prop typed Ref<TokenDetailsSheetBase> | undefined, a caller could legitimately pass a callback ref — and this branch silently skips the close() call rather than routing through the imperative handle. The guard was carried over from the pre-migration code without re-evaluation.
Suggested fix: Either (a) use a local inner ref and call localRef.current?.close() (consistent with other sheets), or (b) if direct RefObject access is intentional, narrow explicitly and add a comment explaining why function refs are excluded.
File: packages/widget/src/components/TokenList/TokenDetailsSheetContent.tsx close button handler (~line 47).
💡 Once you've addressed the items above, re-apply the "Agent Review Request" label to trigger an automated re-review.
There was a problem hiding this comment.
Requesting changes on 2 items — each requires either a code fix or an explicit acceptance comment with justification before this review is considered complete.
| # | Severity | Type | Issue / File |
|---|---|---|---|
| 1 | 🟠 Medium | Code | ExchangeRateBottomSheet.tsx — unsafe RefObject cast in handlers bypasses React 19 ref type safety |
| 2 | 🟢 Low | Code | TokenDetailsSheetContent.tsx — stale typeof ref !== 'function' guard is dead code in React 19 |
1. [Medium] ExchangeRateBottomSheet.tsx — unsafe RefObject cast in handlers
handleContinue, handleCancel, and handleClose all cast ref as RefObject<ExchangeRateBottomSheetBase> then call .current?.close(...). The ref prop is typed Ref<ExchangeRateBottomSheetBase> which is RefObject | RefCallback | null. The cast silently does nothing if a callback ref is passed (no .current on a RefCallback) and will throw if ref is undefined. The component already holds a bottomSheetRef via useRef — use that instead of calling into the outer ref directly. Additionally, handleContinue is not wrapped in useCallback while handleCancel and handleClose are — inconsistent stability profile for three sibling handlers.
Suggestion: Replace all (ref as RefObject<ExchangeRateBottomSheetBase>).current?.close(...) with bottomSheetRef.current?.close(...). Wrap handleContinue in useCallback([onContinue]).
2. [Low] TokenDetailsSheetContent.tsx — stale typeof ref !== 'function' guard
The close button handler contains if (ref && typeof ref !== 'function') { ref.current?.close() }. This guard was required in the forwardRef era to defend against legacy callback refs. Now that ref is a plain prop typed Ref<TokenDetailsSheetBase> | undefined, a caller legitimately passing a callback ref will have the close silently skipped. Use a local inner ref and call localRef.current?.close() instead (consistent with ExchangeRateBottomSheet and TokenValueBottomSheet), or document explicitly why function refs are excluded.
💡 Once you've addressed the items above, re-apply the "Agent Review Request" label to trigger an automated re-review.
- ExchangeRateBottomSheet: extract a stable local close() used by both useImperativeHandle and the continue/cancel/close handlers; drop the unsafe (ref as RefObject) cast and make all three handlers useCallback. Preserves the resolver call (using bottomSheetRef.close() directly would have skipped resolving the awaited promise). - TokenDetailsSheet: pass an onClose callback to TokenDetailsSheetContent instead of forwarding ref, removing the stale 'typeof ref !== function' guard that silently swallowed callback-ref callers.
|
Addressed both QA items in #1
#2 Verified: |
There was a problem hiding this comment.
✅ QA re-review pass — both previously flagged items resolved in commit 28635d6: unsafe RefObject cast removed from ExchangeRateBottomSheet (stable close() extracted), stale forwardRef guard eliminated from TokenDetailsSheetContent (prop-level API refactor). No new issues found.
Which Linear task is linked to this PR?
EMB-456
Why was it implemented this way?
Formalizes the move off React 18 — the repo already ran on React 19.2.7, so this makes it official and adopts React 19 idioms:
react/react-dompeer deps narrowed>=18→>=19(widget,wallet-management,widget-light).forwardRefremoved from 11 components (ref passed as a prop;useImperativeHandlekept).<Context.Provider>→<Context>anduseContext→use()across packages.ResizeObserver→ React 19 ref-cleanup callback.widget-provider-*packages use React-19-only APIs (use()), so they now declare areact >=19peer dependency.Bumped
minor(changeset): v4 is already released, so the React-version floor is treated as a support-matrix change, not a breaking API change.The Rust/oxc React Compiler was evaluated and deferred — it's a no-op in the rolldown bundler path at current versions (experimental); the Babel path was intentionally not used.
Also refreshes a few stale facts in
CLAUDE.md(separatedocs:commit).Visual showcase (Screenshots or Videos)
No visual change. Verified:
check:types+ Biome across all packages, 40 widget tests, full build, and browser smoke of the default + drawer-refpaths.Checklist before requesting a review