Skip to content
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

Don't diff memoized host components in completion phase #13423

Merged
merged 6 commits into from
Aug 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 69 additions & 11 deletions packages/react-dom/src/__tests__/ReactDOMFiber-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ describe('ReactDOMFiber', () => {

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(() => {
document.body.removeChild(container);
container = null;
});

it('should render strings as children', () => {
Expand Down Expand Up @@ -205,12 +211,12 @@ describe('ReactDOMFiber', () => {
};

const assertNamespacesMatch = function(tree) {
container = document.createElement('div');
let testContainer = document.createElement('div');
svgEls = [];
htmlEls = [];
mathEls = [];

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

ReactDOM.unmountComponentAtNode(container);
expect(container.innerHTML).toBe('');
ReactDOM.unmountComponentAtNode(testContainer);
expect(testContainer.innerHTML).toBe('');
};

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

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

let ops = [];
Expand Down Expand Up @@ -944,7 +949,6 @@ describe('ReactDOMFiber', () => {
'leave parent', // Only when we leave the portal does onMouseLeave fire.
]);
} finally {
document.body.removeChild(container);
document.body.removeChild(portalContainer);
}
});
Expand Down Expand Up @@ -987,8 +991,6 @@ describe('ReactDOMFiber', () => {
});

it('should not update event handlers until commit', () => {
document.body.appendChild(container);
try {
let ops = [];
const handlerA = () => ops.push('A');
const handlerB = () => ops.push('B');
Expand Down Expand Up @@ -1060,9 +1062,6 @@ describe('ReactDOMFiber', () => {
// Any click that happens after commit, should invoke A.
node.click();
expect(ops).toEqual(['A']);
} finally {
document.body.removeChild(container);
}
});

it('should not crash encountering low-priority tree', () => {
Expand Down Expand Up @@ -1178,4 +1177,63 @@ describe('ReactDOMFiber', () => {
container.appendChild(fragment);
expect(container.innerHTML).toBe('<div>foo</div>');
});

// Regression test for https://github.com/facebook/react/issues/12643#issuecomment-413727104
it('should not diff memoized host components', () => {
let inputRef = React.createRef();
let didCallOnChange = false;

class Child extends React.Component {
state = {};
componentDidMount() {
document.addEventListener('click', this.update, true);
}
componentWillUnmount() {
document.removeEventListener('click', this.update, true);
}
update = () => {
// We're testing that this setState()
// doesn't cause React to commit updates
// to the input outside (which would itself
// prevent the parent's onChange parent handler
// from firing).
this.setState({});
// Note that onChange was always broken when there was an
// earlier setState() in a manual document capture phase
// listener *in the same component*. But that's very rare.
// Here we're testing that a *child* component doesn't break
// the parent if this happens.
};
render() {
return <div />;
}
}

class Parent extends React.Component {
handleChange = val => {
didCallOnChange = true;
};
render() {
return (
<div>
<Child />
<input
ref={inputRef}
type="checkbox"
checked={true}
onChange={this.handleChange}
/>
</div>
);
}
}

ReactDOM.render(<Parent />, container);
inputRef.current.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
}),
);
expect(didCallOnChange).toBe(true);
});
});
25 changes: 25 additions & 0 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ if (__DEV__) {
function createReactNoop(reconciler: Function, useMutation: boolean) {
let scheduledCallback = null;
let instanceCounter = 0;
let hostDiffCounter = 0;
let hostUpdateCounter = 0;

function appendChildToContainerOrInstance(
parentInstance: Container | Instance,
Expand Down Expand Up @@ -220,6 +222,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
if (newProps === null) {
throw new Error('Should have new props');
}
hostDiffCounter++;
return UPDATE_SIGNAL;
},

Expand Down Expand Up @@ -303,6 +306,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
if (oldProps === null) {
throw new Error('Should have old props');
}
hostUpdateCounter++;
instance.prop = newProps.prop;
},

Expand All @@ -311,6 +315,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
oldText: string,
newText: string,
): void {
hostUpdateCounter++;
textInstance.text = newText;
},

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

flushWithHostCounters(
fn: () => void,
): {|
hostDiffCounter: number,
hostUpdateCounter: number,
|} {
hostDiffCounter = 0;
hostUpdateCounter = 0;
try {
ReactNoop.flush();
return {
hostDiffCounter,
hostUpdateCounter,
};
} finally {
hostDiffCounter = 0;
hostUpdateCounter = 0;
}
},

expire(ms: number): Array<mixed> {
ReactNoop.advanceTime(ms);
return ReactNoop.flushExpired();
Expand Down
2 changes: 2 additions & 0 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ function completeWork(
// If we have an alternate, that means this is an update and we need to
// schedule a side-effect to do the updates.
const oldProps = current.memoizedProps;
if (oldProps !== newProps) {
// If we get updated because one of our children updated, we don't
// have newProps so we'll have to reuse them.
// TODO: Split the update API as separate for the props vs. children.
Expand Down Expand Up @@ -389,6 +390,7 @@ function completeWork(
rootContainerInstance,
currentHostContext,
);
}

if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/

'use strict';

let React;
let ReactNoop;

describe('ReactIncrementalUpdatesMinimalism', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
});

it('should render a simple component', () => {
function Child() {
return <div>Hello World</div>;
}

function Parent() {
return <Child />;
}

ReactNoop.render(<Parent />);
expect(ReactNoop.flushWithHostCounters()).toEqual({
hostDiffCounter: 0,
hostUpdateCounter: 0,
});

ReactNoop.render(<Parent />);
expect(ReactNoop.flushWithHostCounters()).toEqual({
hostDiffCounter: 1,
hostUpdateCounter: 1,
});
});

it('should not diff referentially equal host elements', () => {
function Leaf(props) {
return (
<span>
hello
<b />
{props.name}
</span>
);
}

const constEl = (
<div>
<Leaf name="world" />
</div>
);

function Child() {
return constEl;
}

function Parent() {
return <Child />;
}

ReactNoop.render(<Parent />);
expect(ReactNoop.flushWithHostCounters()).toEqual({
hostDiffCounter: 0,
hostUpdateCounter: 0,
});

ReactNoop.render(<Parent />);
expect(ReactNoop.flushWithHostCounters()).toEqual({
hostDiffCounter: 0,
hostUpdateCounter: 0,
});
});

it('should not diff parents of setState targets', () => {
let childInst;

function Leaf(props) {
return (
<span>
hello
<b />
{props.name}
</span>
);
}

class Child extends React.Component {
state = {name: 'Batman'};
render() {
childInst = this;
return (
<div>
<Leaf name={this.state.name} />
</div>
);
}
}

function Parent() {
return (
<section>
<div>
<Leaf name="world" />
<Child />
<hr />
<Leaf name="world" />
</div>
</section>
);
}

ReactNoop.render(<Parent />);
expect(ReactNoop.flushWithHostCounters()).toEqual({
hostDiffCounter: 0,
hostUpdateCounter: 0,
});

childInst.setState({name: 'Robin'});
expect(ReactNoop.flushWithHostCounters()).toEqual({
// Child > div
// Child > Leaf > span
// Child > Leaf > span > b
hostDiffCounter: 3,
// Child > div
// Child > Leaf > span
// Child > Leaf > span > b
// Child > Leaf > span > #text
hostUpdateCounter: 4,
});

ReactNoop.render(<Parent />);
expect(ReactNoop.flushWithHostCounters()).toEqual({
// Parent > section
// Parent > section > div
// Parent > section > div > Leaf > span
// Parent > section > div > Leaf > span > b
// Parent > section > div > Child > div
// Parent > section > div > Child > div > Leaf > span
// Parent > section > div > Child > div > Leaf > span > b
// Parent > section > div > hr
// Parent > section > div > Leaf > span
// Parent > section > div > Leaf > span > b
hostDiffCounter: 10,
hostUpdateCounter: 10,
});
});
});
Loading