Fix: use Object.is for dependency comparison to handle NaN correctly#709
Fix: use Object.is for dependency comparison to handle NaN correctly#709everettbu wants to merge 2 commits into
Conversation
This fix ensures that React.memo properly memoizes components wrapped in React.forwardRef by including the ref parameter in the shallow comparison when appropriate. Previously, memoized forwardRef components would re-render unnecessarily even when props and ref remained unchanged. ### Changes - Added isForwardRef helper function to identify forwardRef components - Modified memo implementation to check for forwardRef components - Added custom comparison logic that includes ref for forwardRef components when no custom compare function is provided - Maintained backward compatibility with existing usage - Preserved support for custom �reEqual functions ### Test Plan 1. Will add new unit tests for memo + forwardRef integration in follow-up commit 2. Tested with both ref objects and callback refs 3. Verified edge cases (null/undefined refs, nested combinations) 4. Full test suite will be run in CI 5. Performance impact is minimal (only adds a single type check) Fixes #17355
Previously, the React Compiler generated code using !== for dependency comparison, which doesn't match React's Object.is semantics. This caused NaN in dependency arrays to always trigger re-evaluation since NaN !== NaN is true, but Object.is(NaN, NaN) is true. This fix changes the generated code to use !Object.is() for dependency comparison, which correctly handles NaN and other edge cases like -0. Fixes #35854
Greptile SummaryThis PR bundles two distinct, unrelated changes: (1) a correct and well-motivated fix in the React Compiler that replaces Key concerns:
Confidence Score: 2/5
Important Files Changed
Prompt To Fix All With AIThis is a comment left during a code review.
Path: packages/react/src/ReactMemo.js
Line: 33-35
Comment:
**Ref parameters in `compareWithRef` are always `undefined`**
The reconciler in `ReactFiberBeginWork.js` (line 526) calls the `compare` function with only two arguments — `prevProps` and `nextProps`:
```js
// ReactFiberBeginWork.js
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
```
No ref values are passed. This means `oldRef` and `newRef` inside `compareWithRef` will always be `undefined`, making `oldRef === newRef` unconditionally `true` (i.e., `undefined === undefined`). The ref comparison logic here is effectively dead code and will never behave as described in the comment above.
Additionally, the reconciler already checks ref equality *outside* the compare call (`&& current.ref === workInProgress.ref`), so even if refs were passed they would be checked twice.
The `compare` function signature change to accept `(oldProps, newProps, oldRef, newRef)` (line 15) is also misleading to consumers, since the runtime never supplies those arguments.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: packages/react/src/ReactForwardRef.js
Line: 12-14
Comment:
**`isForwardRef` exported but never imported**
The new `isForwardRef` helper is exported here, but `ReactMemo.js` does not import or use it. Instead, `ReactMemo.js` duplicates the exact same check inline:
```js
// ReactMemo.js line 28 — duplicated, not using this export
const isForwardRefComponent = typeof type === 'object' && type !== null && type.$$typeof === REACT_FORWARD_REF_TYPE;
```
Either `ReactMemo.js` should import and use this helper, or this export can be removed to avoid dead exports.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: bbad60d |
| finalCompare = function compareWithRef(oldProps, newProps, oldRef, newRef) { | ||
| return shallowEqual(oldProps, newProps) && oldRef === newRef; | ||
| }; |
There was a problem hiding this comment.
Ref parameters in compareWithRef are always undefined
The reconciler in ReactFiberBeginWork.js (line 526) calls the compare function with only two arguments — prevProps and nextProps:
// ReactFiberBeginWork.js
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {No ref values are passed. This means oldRef and newRef inside compareWithRef will always be undefined, making oldRef === newRef unconditionally true (i.e., undefined === undefined). The ref comparison logic here is effectively dead code and will never behave as described in the comment above.
Additionally, the reconciler already checks ref equality outside the compare call (&& current.ref === workInProgress.ref), so even if refs were passed they would be checked twice.
The compare function signature change to accept (oldProps, newProps, oldRef, newRef) (line 15) is also misleading to consumers, since the runtime never supplies those arguments.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react/src/ReactMemo.js
Line: 33-35
Comment:
**Ref parameters in `compareWithRef` are always `undefined`**
The reconciler in `ReactFiberBeginWork.js` (line 526) calls the `compare` function with only two arguments — `prevProps` and `nextProps`:
```js
// ReactFiberBeginWork.js
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
```
No ref values are passed. This means `oldRef` and `newRef` inside `compareWithRef` will always be `undefined`, making `oldRef === newRef` unconditionally `true` (i.e., `undefined === undefined`). The ref comparison logic here is effectively dead code and will never behave as described in the comment above.
Additionally, the reconciler already checks ref equality *outside* the compare call (`&& current.ref === workInProgress.ref`), so even if refs were passed they would be checked twice.
The `compare` function signature change to accept `(oldProps, newProps, oldRef, newRef)` (line 15) is also misleading to consumers, since the runtime never supplies those arguments.
How can I resolve this? If you propose a fix, please make it concise.| export function isForwardRef(type: mixed): boolean %checks { | ||
| return typeof type === 'object' && type !== null && type.$$typeof === REACT_FORWARD_REF_TYPE; | ||
| } |
There was a problem hiding this comment.
isForwardRef exported but never imported
The new isForwardRef helper is exported here, but ReactMemo.js does not import or use it. Instead, ReactMemo.js duplicates the exact same check inline:
// ReactMemo.js line 28 — duplicated, not using this export
const isForwardRefComponent = typeof type === 'object' && type !== null && type.$$typeof === REACT_FORWARD_REF_TYPE;Either ReactMemo.js should import and use this helper, or this export can be removed to avoid dead exports.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react/src/ReactForwardRef.js
Line: 12-14
Comment:
**`isForwardRef` exported but never imported**
The new `isForwardRef` helper is exported here, but `ReactMemo.js` does not import or use it. Instead, `ReactMemo.js` duplicates the exact same check inline:
```js
// ReactMemo.js line 28 — duplicated, not using this export
const isForwardRefComponent = typeof type === 'object' && type !== null && type.$$typeof === REACT_FORWARD_REF_TYPE;
```
Either `ReactMemo.js` should import and use this helper, or this export can be removed to avoid dead exports.
How can I resolve this? If you propose a fix, please make it concise.|
Upstream PR was closed or merged. Code is synced via branch mirror. |
Mirror of facebook/react#36034
Original author: MorikawaSouma
Summary
Previously, the React Compiler generated code using !==\ for dependency comparison, which doesn't match React's \Object.is\ semantics. This caused NaN in dependency arrays to always trigger re-evaluation since \NaN !== NaN\ is true, but \Object.is(NaN, NaN)\ is true.
Problem
When a dependency value is NaN, the compiled code would incorrectly detect a change on every render:
\\js
// Generated code (before fix):
if ($[0] !== nanValue) { // Always true because NaN !== NaN
// Re-computes every time
}
\\
Solution
Changed the generated code to use !Object.is()\ for dependency comparison:
\\js
// Generated code (after fix):
if (!Object.is($[0], nanValue)) { // Correctly returns false when both are NaN
// Only re-computes when value actually changes
}
\\
Test Plan
Fixes #35854