-
Notifications
You must be signed in to change notification settings - Fork 0
Fix React.memo not working with forwardRef components #686
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,11 +7,12 @@ | |
| * @noflow | ||
| */ | ||
|
|
||
| import {REACT_MEMO_TYPE} from 'shared/ReactSymbols'; | ||
| import {REACT_MEMO_TYPE, REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols'; | ||
| import shallowEqual from 'shared/shallowEqual'; | ||
|
|
||
| export function memo<Props>( | ||
| type: React$ElementType, | ||
| compare?: (oldProps: Props, newProps: Props) => boolean, | ||
| compare?: (oldProps: Props, newProps: Props, oldRef: mixed, newRef: mixed) => boolean, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Misleading public API type signature change The compare?: (oldProps: Props, newProps: Props, oldRef: mixed, newRef: mixed) => boolean,However, as described above, the reconciler never passes these arguments — they will always be Prompt To Fix With AIThis is a comment left during a code review.
Path: packages/react/src/ReactMemo.js
Line: 15
Comment:
**Misleading public API type signature change**
The `compare` callback type has been changed to include `oldRef` and `newRef` parameters:
```js
compare?: (oldProps: Props, newProps: Props, oldRef: mixed, newRef: mixed) => boolean,
```
However, as described above, the reconciler never passes these arguments — they will always be `undefined` when `compare` is invoked. Any user who reads this type signature and writes a custom `compare` function that relies on `oldRef`/`newRef` will find those parameters are always `undefined`, leading to subtle, hard-to-debug bugs. The public type signature should not be changed to advertise parameters that are never populated.
How can I resolve this? If you propose a fix, please make it concise. |
||
| ) { | ||
| if (__DEV__) { | ||
| if (type == null) { | ||
|
|
@@ -22,10 +23,22 @@ export function memo<Props>( | |
| ); | ||
| } | ||
| } | ||
|
|
||
| // Create custom compare function that includes ref for forwardRef components | ||
| const isForwardRefComponent = typeof type === 'object' && type !== null && type.$$typeof === REACT_FORWARD_REF_TYPE; | ||
| let finalCompare = compare; | ||
|
|
||
| if (isForwardRefComponent && compare === undefined) { | ||
| // Default compare for forwardRef: shallow equal props + strict equal ref | ||
| finalCompare = function compareWithRef(oldProps, newProps, oldRef, newRef) { | ||
| return shallowEqual(oldProps, newProps) && oldRef === newRef; | ||
| }; | ||
| } | ||
|
Comment on lines
+31
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fix is a no-op — the reconciler never passes ref arguments to The custom // ReactFiberBeginWork.js ~line 526
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {Because Furthermore, the reconciler already compares refs independently with Prompt To Fix With AIThis is a comment left during a code review.
Path: packages/react/src/ReactMemo.js
Line: 31-36
Comment:
**The fix is a no-op — the reconciler never passes ref arguments to `compare`**
The custom `compareWithRef` function accepts `(oldProps, newProps, oldRef, newRef)`, but the React reconciler in `ReactFiberBeginWork.js` always invokes the `compare` function with **only two arguments**:
```js
// ReactFiberBeginWork.js ~line 526
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
```
Because `oldRef` and `newRef` are never passed by the reconciler, they will always be `undefined` inside `compareWithRef`. The check `oldRef === newRef` therefore always evaluates to `undefined === undefined` → `true`. This makes `finalCompare` functionally equivalent to just `shallowEqual(oldProps, newProps)` — which is **already the default behavior** when `compare` is `null` (the reconciler falls back to `shallowEqual`).
Furthermore, the reconciler already compares refs independently with `current.ref === workInProgress.ref` on the same line, so ref-based bailing-out already works without any changes here. This PR does not change the observable behavior of `React.memo` at all.
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| const elementType = { | ||
| $$typeof: REACT_MEMO_TYPE, | ||
| type, | ||
| compare: compare === undefined ? null : compare, | ||
| compare: finalCompare === undefined ? null : finalCompare, | ||
| }; | ||
| if (__DEV__) { | ||
| let ownName; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isForwardRefhelper is dead code — never imported or usedThe new
isForwardRefexport is never imported anywhere.ReactMemo.jsduplicates the same check inline:If the intent was to share this utility,
ReactMemo.jsshould import and useisForwardReffrom this file instead of reimplementing it. As written, this export is unreachable dead code.Prompt To Fix With AI