Skip to content

Commit d2f5c3f

Browse files
authored
Don't diff memoized host components in completion phase (#13423)
* Add a regression test for 12643#issuecomment-413727104 * Don't diff memoized host components * Add regression tests for noop renderer * No early return * Strengthen the test for host siblings * Flow types
1 parent 5e0f073 commit d2f5c3f

File tree

5 files changed

+345
-104
lines changed

5 files changed

+345
-104
lines changed

packages/react-dom/src/__tests__/ReactDOMFiber-test.js

Lines changed: 125 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ describe('ReactDOMFiber', () => {
1818

1919
beforeEach(() => {
2020
container = document.createElement('div');
21+
document.body.appendChild(container);
22+
});
23+
24+
afterEach(() => {
25+
document.body.removeChild(container);
26+
container = null;
2127
});
2228

2329
it('should render strings as children', () => {
@@ -205,12 +211,12 @@ describe('ReactDOMFiber', () => {
205211
};
206212

207213
const assertNamespacesMatch = function(tree) {
208-
container = document.createElement('div');
214+
let testContainer = document.createElement('div');
209215
svgEls = [];
210216
htmlEls = [];
211217
mathEls = [];
212218

213-
ReactDOM.render(tree, container);
219+
ReactDOM.render(tree, testContainer);
214220
svgEls.forEach(el => {
215221
expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg');
216222
});
@@ -221,8 +227,8 @@ describe('ReactDOMFiber', () => {
221227
expect(el.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
222228
});
223229

224-
ReactDOM.unmountComponentAtNode(container);
225-
expect(container.innerHTML).toBe('');
230+
ReactDOM.unmountComponentAtNode(testContainer);
231+
expect(testContainer.innerHTML).toBe('');
226232
};
227233

228234
it('should render one portal', () => {
@@ -874,7 +880,6 @@ describe('ReactDOMFiber', () => {
874880

875881
it('should not onMouseLeave when staying in the portal', () => {
876882
const portalContainer = document.createElement('div');
877-
document.body.appendChild(container);
878883
document.body.appendChild(portalContainer);
879884

880885
let ops = [];
@@ -944,7 +949,6 @@ describe('ReactDOMFiber', () => {
944949
'leave parent', // Only when we leave the portal does onMouseLeave fire.
945950
]);
946951
} finally {
947-
document.body.removeChild(container);
948952
document.body.removeChild(portalContainer);
949953
}
950954
});
@@ -987,82 +991,77 @@ describe('ReactDOMFiber', () => {
987991
});
988992

989993
it('should not update event handlers until commit', () => {
990-
document.body.appendChild(container);
991-
try {
992-
let ops = [];
993-
const handlerA = () => ops.push('A');
994-
const handlerB = () => ops.push('B');
995-
996-
class Example extends React.Component {
997-
state = {flip: false, count: 0};
998-
flip() {
999-
this.setState({flip: true, count: this.state.count + 1});
1000-
}
1001-
tick() {
1002-
this.setState({count: this.state.count + 1});
1003-
}
1004-
render() {
1005-
const useB = !this.props.forceA && this.state.flip;
1006-
return <div onClick={useB ? handlerB : handlerA} />;
1007-
}
994+
let ops = [];
995+
const handlerA = () => ops.push('A');
996+
const handlerB = () => ops.push('B');
997+
998+
class Example extends React.Component {
999+
state = {flip: false, count: 0};
1000+
flip() {
1001+
this.setState({flip: true, count: this.state.count + 1});
10081002
}
1003+
tick() {
1004+
this.setState({count: this.state.count + 1});
1005+
}
1006+
render() {
1007+
const useB = !this.props.forceA && this.state.flip;
1008+
return <div onClick={useB ? handlerB : handlerA} />;
1009+
}
1010+
}
10091011

1010-
class Click extends React.Component {
1011-
constructor() {
1012-
super();
1013-
node.click();
1014-
}
1015-
render() {
1016-
return null;
1017-
}
1012+
class Click extends React.Component {
1013+
constructor() {
1014+
super();
1015+
node.click();
10181016
}
1017+
render() {
1018+
return null;
1019+
}
1020+
}
10191021

1020-
let inst;
1021-
ReactDOM.render([<Example key="a" ref={n => (inst = n)} />], container);
1022-
const node = container.firstChild;
1023-
expect(node.tagName).toEqual('DIV');
1022+
let inst;
1023+
ReactDOM.render([<Example key="a" ref={n => (inst = n)} />], container);
1024+
const node = container.firstChild;
1025+
expect(node.tagName).toEqual('DIV');
10241026

1025-
node.click();
1027+
node.click();
10261028

1027-
expect(ops).toEqual(['A']);
1028-
ops = [];
1029+
expect(ops).toEqual(['A']);
1030+
ops = [];
10291031

1030-
// Render with the other event handler.
1031-
inst.flip();
1032+
// Render with the other event handler.
1033+
inst.flip();
10321034

1033-
node.click();
1035+
node.click();
10341036

1035-
expect(ops).toEqual(['B']);
1036-
ops = [];
1037+
expect(ops).toEqual(['B']);
1038+
ops = [];
10371039

1038-
// Rerender without changing any props.
1039-
inst.tick();
1040+
// Rerender without changing any props.
1041+
inst.tick();
10401042

1041-
node.click();
1043+
node.click();
10421044

1043-
expect(ops).toEqual(['B']);
1044-
ops = [];
1045+
expect(ops).toEqual(['B']);
1046+
ops = [];
10451047

1046-
// Render a flip back to the A handler. The second component invokes the
1047-
// click handler during render to simulate a click during an aborted
1048-
// render. I use this hack because at current time we don't have a way to
1049-
// test aborted ReactDOM renders.
1050-
ReactDOM.render(
1051-
[<Example key="a" forceA={true} />, <Click key="b" />],
1052-
container,
1053-
);
1048+
// Render a flip back to the A handler. The second component invokes the
1049+
// click handler during render to simulate a click during an aborted
1050+
// render. I use this hack because at current time we don't have a way to
1051+
// test aborted ReactDOM renders.
1052+
ReactDOM.render(
1053+
[<Example key="a" forceA={true} />, <Click key="b" />],
1054+
container,
1055+
);
10541056

1055-
// Because the new click handler has not yet committed, we should still
1056-
// invoke B.
1057-
expect(ops).toEqual(['B']);
1058-
ops = [];
1057+
// Because the new click handler has not yet committed, we should still
1058+
// invoke B.
1059+
expect(ops).toEqual(['B']);
1060+
ops = [];
10591061

1060-
// Any click that happens after commit, should invoke A.
1061-
node.click();
1062-
expect(ops).toEqual(['A']);
1063-
} finally {
1064-
document.body.removeChild(container);
1065-
}
1062+
// Any click that happens after commit, should invoke A.
1063+
node.click();
1064+
expect(ops).toEqual(['A']);
10661065
});
10671066

10681067
it('should not crash encountering low-priority tree', () => {
@@ -1178,4 +1177,63 @@ describe('ReactDOMFiber', () => {
11781177
container.appendChild(fragment);
11791178
expect(container.innerHTML).toBe('<div>foo</div>');
11801179
});
1180+
1181+
// Regression test for https://github.com/facebook/react/issues/12643#issuecomment-413727104
1182+
it('should not diff memoized host components', () => {
1183+
let inputRef = React.createRef();
1184+
let didCallOnChange = false;
1185+
1186+
class Child extends React.Component {
1187+
state = {};
1188+
componentDidMount() {
1189+
document.addEventListener('click', this.update, true);
1190+
}
1191+
componentWillUnmount() {
1192+
document.removeEventListener('click', this.update, true);
1193+
}
1194+
update = () => {
1195+
// We're testing that this setState()
1196+
// doesn't cause React to commit updates
1197+
// to the input outside (which would itself
1198+
// prevent the parent's onChange parent handler
1199+
// from firing).
1200+
this.setState({});
1201+
// Note that onChange was always broken when there was an
1202+
// earlier setState() in a manual document capture phase
1203+
// listener *in the same component*. But that's very rare.
1204+
// Here we're testing that a *child* component doesn't break
1205+
// the parent if this happens.
1206+
};
1207+
render() {
1208+
return <div />;
1209+
}
1210+
}
1211+
1212+
class Parent extends React.Component {
1213+
handleChange = val => {
1214+
didCallOnChange = true;
1215+
};
1216+
render() {
1217+
return (
1218+
<div>
1219+
<Child />
1220+
<input
1221+
ref={inputRef}
1222+
type="checkbox"
1223+
checked={true}
1224+
onChange={this.handleChange}
1225+
/>
1226+
</div>
1227+
);
1228+
}
1229+
}
1230+
1231+
ReactDOM.render(<Parent />, container);
1232+
inputRef.current.dispatchEvent(
1233+
new MouseEvent('click', {
1234+
bubbles: true,
1235+
}),
1236+
);
1237+
expect(didCallOnChange).toBe(true);
1238+
});
11811239
});

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ if (__DEV__) {
4141
function createReactNoop(reconciler: Function, useMutation: boolean) {
4242
let scheduledCallback = null;
4343
let instanceCounter = 0;
44+
let hostDiffCounter = 0;
45+
let hostUpdateCounter = 0;
4446

4547
function appendChildToContainerOrInstance(
4648
parentInstance: Container | Instance,
@@ -220,6 +222,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
220222
if (newProps === null) {
221223
throw new Error('Should have new props');
222224
}
225+
hostDiffCounter++;
223226
return UPDATE_SIGNAL;
224227
},
225228

@@ -303,6 +306,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
303306
if (oldProps === null) {
304307
throw new Error('Should have old props');
305308
}
309+
hostUpdateCounter++;
306310
instance.prop = newProps.prop;
307311
},
308312

@@ -311,6 +315,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
311315
oldText: string,
312316
newText: string,
313317
): void {
318+
hostUpdateCounter++;
314319
textInstance.text = newText;
315320
},
316321

@@ -556,6 +561,26 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
556561
return actual !== null ? actual : [];
557562
},
558563

564+
flushWithHostCounters(
565+
fn: () => void,
566+
): {|
567+
hostDiffCounter: number,
568+
hostUpdateCounter: number,
569+
|} {
570+
hostDiffCounter = 0;
571+
hostUpdateCounter = 0;
572+
try {
573+
ReactNoop.flush();
574+
return {
575+
hostDiffCounter,
576+
hostUpdateCounter,
577+
};
578+
} finally {
579+
hostDiffCounter = 0;
580+
hostUpdateCounter = 0;
581+
}
582+
},
583+
559584
expire(ms: number): Array<mixed> {
560585
ReactNoop.advanceTime(ms);
561586
return ReactNoop.flushExpired();

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -361,34 +361,36 @@ function completeWork(
361361
// If we have an alternate, that means this is an update and we need to
362362
// schedule a side-effect to do the updates.
363363
const oldProps = current.memoizedProps;
364-
// If we get updated because one of our children updated, we don't
365-
// have newProps so we'll have to reuse them.
366-
// TODO: Split the update API as separate for the props vs. children.
367-
// Even better would be if children weren't special cased at all tho.
368-
const instance: Instance = workInProgress.stateNode;
369-
const currentHostContext = getHostContext();
370-
// TODO: Experiencing an error where oldProps is null. Suggests a host
371-
// component is hitting the resume path. Figure out why. Possibly
372-
// related to `hidden`.
373-
const updatePayload = prepareUpdate(
374-
instance,
375-
type,
376-
oldProps,
377-
newProps,
378-
rootContainerInstance,
379-
currentHostContext,
380-
);
364+
if (oldProps !== newProps) {
365+
// If we get updated because one of our children updated, we don't
366+
// have newProps so we'll have to reuse them.
367+
// TODO: Split the update API as separate for the props vs. children.
368+
// Even better would be if children weren't special cased at all tho.
369+
const instance: Instance = workInProgress.stateNode;
370+
const currentHostContext = getHostContext();
371+
// TODO: Experiencing an error where oldProps is null. Suggests a host
372+
// component is hitting the resume path. Figure out why. Possibly
373+
// related to `hidden`.
374+
const updatePayload = prepareUpdate(
375+
instance,
376+
type,
377+
oldProps,
378+
newProps,
379+
rootContainerInstance,
380+
currentHostContext,
381+
);
381382

382-
updateHostComponent(
383-
current,
384-
workInProgress,
385-
updatePayload,
386-
type,
387-
oldProps,
388-
newProps,
389-
rootContainerInstance,
390-
currentHostContext,
391-
);
383+
updateHostComponent(
384+
current,
385+
workInProgress,
386+
updatePayload,
387+
type,
388+
oldProps,
389+
newProps,
390+
rootContainerInstance,
391+
currentHostContext,
392+
);
393+
}
392394

393395
if (current.ref !== workInProgress.ref) {
394396
markRef(workInProgress);

0 commit comments

Comments
 (0)