Skip to content

Commit 15df051

Browse files
authored
Add warning if return pointer is inconsistent (#20219)
Bugs caused by inconsistent return pointers are tricky to diagnose because the source of the error is often in a different part of the codebase from the actual mistake. For example, you might forget to set a return pointer during the render phase, which later causes a crash in the commit phase. This adds a dev-only invariant to the commit phase to check for inconsistencies. With this in place, we'll hopefully catch return pointer errors quickly during local development, when we have the most context for what might have caused it.
1 parent 9aca239 commit 15df051

File tree

3 files changed

+93
-0
lines changed

3 files changed

+93
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
let React;
11+
let ReactNoop;
12+
13+
beforeEach(() => {
14+
React = require('react');
15+
ReactNoop = require('react-noop-renderer');
16+
});
17+
18+
// Don't feel too guilty if you have to delete this test.
19+
// @gate new
20+
// @gate __DEV__
21+
test('warns in DEV if return pointer is inconsistent', async () => {
22+
const {useRef, useLayoutEffect} = React;
23+
24+
let ref = null;
25+
function App({text}) {
26+
ref = useRef(null);
27+
return (
28+
<>
29+
<Sibling text={text} />
30+
<div ref={ref}>{text}</div>
31+
</>
32+
);
33+
}
34+
35+
function Sibling({text}) {
36+
useLayoutEffect(() => {
37+
if (text === 'B') {
38+
// Mutate the return pointer of the div to point to the wrong alternate.
39+
// This simulates the most common type of return pointer inconsistency.
40+
const current = ref.current.fiber;
41+
const workInProgress = current.alternate;
42+
workInProgress.return = current.return;
43+
}
44+
}, [text]);
45+
return null;
46+
}
47+
48+
const root = ReactNoop.createRoot();
49+
await ReactNoop.act(async () => {
50+
root.render(<App text="A" />);
51+
});
52+
53+
spyOnDev(console, 'error');
54+
await ReactNoop.act(async () => {
55+
root.render(<App text="B" />);
56+
});
57+
expect(console.error.calls.count()).toBe(1);
58+
expect(console.error.calls.argsFor(0)[0]).toMatch(
59+
'Internal React error: Return pointer is inconsistent with parent.',
60+
);
61+
});

packages/react-noop-renderer/src/createReactNoop.js

+5
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
275275
props: Props,
276276
rootContainerInstance: Container,
277277
hostContext: HostContext,
278+
internalInstanceHandle: Object,
278279
): Instance {
279280
if (type === 'errorInCompletePhase') {
280281
throw new Error('Error in host config.');
@@ -300,6 +301,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
300301
value: inst.context,
301302
enumerable: false,
302303
});
304+
Object.defineProperty(inst, 'fiber', {
305+
value: internalInstanceHandle,
306+
enumerable: false,
307+
});
303308
return inst;
304309
},
305310

packages/react-reconciler/src/ReactFiberCommitWork.new.js

+27
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ function iterativelyCommitBeforeMutationEffects_begin() {
463463
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
464464
child !== null
465465
) {
466+
warnIfWrongReturnPointer(fiber, child);
466467
nextEffect = child;
467468
} else {
468469
iterativelyCommitBeforeMutationEffects_complete();
@@ -496,6 +497,7 @@ function iterativelyCommitBeforeMutationEffects_complete() {
496497

497498
const sibling = fiber.sibling;
498499
if (sibling !== null) {
500+
warnIfWrongReturnPointer(fiber.return, sibling);
499501
nextEffect = sibling;
500502
return;
501503
}
@@ -713,6 +715,7 @@ function iterativelyCommitMutationEffects_begin(
713715

714716
const child = fiber.child;
715717
if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) {
718+
warnIfWrongReturnPointer(fiber, child);
716719
nextEffect = child;
717720
} else {
718721
iterativelyCommitMutationEffects_complete(root, renderPriorityLevel);
@@ -751,6 +754,7 @@ function iterativelyCommitMutationEffects_complete(
751754

752755
const sibling = fiber.sibling;
753756
if (sibling !== null) {
757+
warnIfWrongReturnPointer(fiber.return, sibling);
754758
nextEffect = sibling;
755759
return;
756760
}
@@ -1172,12 +1176,14 @@ function iterativelyCommitLayoutEffects_begin(
11721176
}
11731177
const sibling = finishedWork.sibling;
11741178
if (sibling !== null) {
1179+
warnIfWrongReturnPointer(finishedWork.return, sibling);
11751180
nextEffect = sibling;
11761181
} else {
11771182
nextEffect = finishedWork.return;
11781183
iterativelyCommitLayoutEffects_complete(subtreeRoot, finishedRoot);
11791184
}
11801185
} else {
1186+
warnIfWrongReturnPointer(finishedWork, firstChild);
11811187
nextEffect = firstChild;
11821188
}
11831189
} else {
@@ -1224,6 +1230,7 @@ function iterativelyCommitLayoutEffects_complete(
12241230

12251231
const sibling = fiber.sibling;
12261232
if (sibling !== null) {
1233+
warnIfWrongReturnPointer(fiber.return, sibling);
12271234
nextEffect = sibling;
12281235
return;
12291236
}
@@ -1757,12 +1764,14 @@ function iterativelyCommitPassiveMountEffects_begin(
17571764
}
17581765
const sibling = fiber.sibling;
17591766
if (sibling !== null) {
1767+
warnIfWrongReturnPointer(fiber.return, sibling);
17601768
nextEffect = sibling;
17611769
} else {
17621770
nextEffect = fiber.return;
17631771
iterativelyCommitPassiveMountEffects_complete(subtreeRoot, root);
17641772
}
17651773
} else {
1774+
warnIfWrongReturnPointer(fiber, firstChild);
17661775
nextEffect = firstChild;
17671776
}
17681777
} else {
@@ -1808,6 +1817,7 @@ function iterativelyCommitPassiveMountEffects_complete(
18081817

18091818
const sibling = fiber.sibling;
18101819
if (sibling !== null) {
1820+
warnIfWrongReturnPointer(fiber.return, sibling);
18111821
nextEffect = sibling;
18121822
return;
18131823
}
@@ -1886,6 +1896,7 @@ function iterativelyCommitPassiveUnmountEffects_begin() {
18861896
}
18871897

18881898
if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && child !== null) {
1899+
warnIfWrongReturnPointer(fiber, child);
18891900
nextEffect = child;
18901901
} else {
18911902
iterativelyCommitPassiveUnmountEffects_complete();
@@ -1904,6 +1915,7 @@ function iterativelyCommitPassiveUnmountEffects_complete() {
19041915

19051916
const sibling = fiber.sibling;
19061917
if (sibling !== null) {
1918+
warnIfWrongReturnPointer(fiber.return, sibling);
19071919
nextEffect = sibling;
19081920
return;
19091921
}
@@ -1941,6 +1953,7 @@ function iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_begin(
19411953
const fiber = nextEffect;
19421954
const child = fiber.child;
19431955
if ((fiber.subtreeFlags & PassiveStatic) !== NoFlags && child !== null) {
1956+
warnIfWrongReturnPointer(fiber, child);
19441957
nextEffect = child;
19451958
} else {
19461959
iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_complete(
@@ -1968,6 +1981,7 @@ function iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_complete(
19681981

19691982
const sibling = fiber.sibling;
19701983
if (sibling !== null) {
1984+
warnIfWrongReturnPointer(fiber.return, sibling);
19711985
nextEffect = sibling;
19721986
return;
19731987
}
@@ -3178,3 +3192,16 @@ function invokeEffectsInDev(
31783192
}
31793193
}
31803194
}
3195+
3196+
let didWarnWrongReturnPointer = false;
3197+
function warnIfWrongReturnPointer(returnFiber, child) {
3198+
if (__DEV__) {
3199+
if (!didWarnWrongReturnPointer && child.return !== returnFiber) {
3200+
didWarnWrongReturnPointer = true;
3201+
console.error(
3202+
'Internal React error: Return pointer is inconsistent ' +
3203+
'with parent.',
3204+
);
3205+
}
3206+
}
3207+
}

0 commit comments

Comments
 (0)