Support React keys in array branches, fixes #2139 (#2157)

Summary:
Release notes: none

Add logic to support adding keys when inlining arrays. Fixes https://github.com/facebook/prepack/issues/2139.
Closes https://github.com/facebook/prepack/pull/2157

Differential Revision: D8732339

Pulled By: trueadm

fbshipit-source-id: cfb1fe8725522e3eba5e1061832ddba8359484bb
This commit is contained in:
Dominic Gannaway 2018-07-04 08:10:33 -07:00 committed by Facebook Github Bot
parent 2c79cb103d
commit ef7e1873fa
8 changed files with 869 additions and 3 deletions

View File

@ -59,8 +59,13 @@ export function getValueWithBranchingLogicApplied(
let needsKeys = false;
// we check the inlined value and see if the component types match
const searchAndFlagMatchingComponentTypes = (xTypeParent, yTypeParent) => {
let [, x, y] = value.args;
const searchAndFlagMatchingComponentTypes = (x, y, xTypeParent, yTypeParent) => {
// The returned value is the result of getting the "render" from a component.
// We need to search the value returned to see if the nodes need keys adding to them.
// 1. If we have <X? /> and <Y? />, then check if their
// types are the same, if they are the same and the parent types
// are not the same as then we need to add keys
if (x instanceof ObjectValue && isReactElement(x) && y instanceof ObjectValue && isReactElement(y)) {
let xType = getProperty(realm, x, "type");
let yType = getProperty(realm, y, "type");
@ -68,6 +73,46 @@ export function getValueWithBranchingLogicApplied(
if (xType.equals(yType) && !xTypeParent.equals(xType) && !yTypeParent.equals(yType)) {
needsKeys = true;
}
} else if (x instanceof ArrayValue) {
// If we have x: []
// Go through the elements of array x
forEachArrayValue(realm, x, (xElem, index) => {
let yElem = y;
// And if we also have y: [], with a given element from x
// search element of y at the same index from x.
// If y is not an array, then continue but use x: [] against y
if (y instanceof ArrayValue) {
yElem = getProperty(realm, y, index + "");
}
searchAndFlagMatchingComponentTypes(xElem, yElem, xTypeParent, yTypeParent);
});
} else if (y instanceof ArrayValue) {
// If we have y: []
// Go through the elements of array y
forEachArrayValue(realm, y, (yElem, index) => {
let xElem = x;
// And if we also have x: [], with a given element from y
// search element of x at the same index from y.
// If x is not an array, then continue but use y: [] against x
if (x instanceof ArrayValue) {
xElem = getProperty(realm, x, index + "");
}
searchAndFlagMatchingComponentTypes(xElem, yElem, xTypeParent, yTypeParent);
});
} else if (x instanceof AbstractValue && x.kind === "conditional") {
// if x is a conditional value like "a ? b : c",
// then recusrively check b and c agaginst that y
let [, consequentVal, alternateVal] = x.args;
searchAndFlagMatchingComponentTypes(consequentVal, y, xTypeParent, yTypeParent);
searchAndFlagMatchingComponentTypes(alternateVal, y, xTypeParent, yTypeParent);
} else if (y instanceof AbstractValue && y.kind === "conditional") {
// if y is a conditional value like "a ? b : c",
// then recusrively check b and c agaginst that x
let [, consequentVal, alternateVal] = y.args;
searchAndFlagMatchingComponentTypes(x, consequentVal, xTypeParent, yTypeParent);
searchAndFlagMatchingComponentTypes(x, alternateVal, xTypeParent, yTypeParent);
}
};
@ -89,7 +134,8 @@ export function getValueWithBranchingLogicApplied(
}
}
} else if (!xType.equals(yType)) {
searchAndFlagMatchingComponentTypes(xType, yType);
let [, xVal, yVal] = value.args;
searchAndFlagMatchingComponentTypes(xVal, yVal, xType, yType);
}
} else if (
ArrayValue.isIntrinsicAndHasWidenedNumericProperty(x) ||

View File

@ -28,6 +28,26 @@ it("Key nesting 3", () => {
runTest(__dirname + "/Reconciliation/key-nesting-3.js");
});
it("Key nesting 4", () => {
runTest(__dirname + "/Reconciliation/key-nesting-4.js");
});
it("Key nesting 5", () => {
runTest(__dirname + "/Reconciliation/key-nesting-5.js");
});
it("Key nesting 6", () => {
runTest(__dirname + "/Reconciliation/key-nesting-6.js");
});
it("Key nesting 7", () => {
runTest(__dirname + "/Reconciliation/key-nesting-7.js");
});
it("Key nesting 8", () => {
runTest(__dirname + "/Reconciliation/key-nesting-8.js");
});
it("Key change", () => {
runTest(__dirname + "/Reconciliation/key-change.js");
});

View File

@ -0,0 +1,40 @@
var React = require('react');
this['React'] = React;
function Foo(props) {
return [<div ref={props.callback} key="0" />];
}
function Bar(props) {
return [<div ref={props.callback} key="0" />];
}
function App(props) {
return props.foo ? <Foo {...props} /> : <Bar {...props} />;
}
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App);
}
App.getTrials = function(renderer, Root) {
let counter = 0;
let nodes = [];
function callback(node) {
nodes.push(node);
counter++;
}
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={false} />);
renderer.update(<Root callback={callback} foo={false} />);
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={true} />);
let results = [];
results.push(['ensure ref is called on every change', counter]);
results.push(['ensure refs are cleared', nodes.map(Boolean)]);
results.push(['ensure refs are different', new Set(nodes).size]);
return results;
};
module.exports = App;

View File

@ -0,0 +1,40 @@
var React = require('react');
this['React'] = React;
function Foo(props) {
return [<div ref={props.callback} key="0" />];
}
function Bar(props) {
return <div ref={props.callback} key="0" />;
}
function App(props) {
return props.foo ? <Foo {...props} /> : <Bar {...props} />;
}
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App);
}
App.getTrials = function(renderer, Root) {
let counter = 0;
let nodes = [];
function callback(node) {
nodes.push(node);
counter++;
}
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={false} />);
renderer.update(<Root callback={callback} foo={false} />);
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={true} />);
let results = [];
results.push(['ensure ref is called on every change', counter]);
results.push(['ensure refs are cleared', nodes.map(Boolean)]);
results.push(['ensure refs are different', new Set(nodes).size]);
return results;
};
module.exports = App;

View File

@ -0,0 +1,40 @@
var React = require('react');
this['React'] = React;
function Foo(props) {
return [[<div ref={props.callback} key="0" />]];
}
function Bar(props) {
return [[<div ref={props.callback} key="0" />]];
}
function App(props) {
return props.foo ? <Foo {...props} /> : <Bar {...props} />;
}
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App);
}
App.getTrials = function(renderer, Root) {
let counter = 0;
let nodes = [];
function callback(node) {
nodes.push(node);
counter++;
}
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={false} />);
renderer.update(<Root callback={callback} foo={false} />);
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={true} />);
let results = [];
results.push(['ensure ref is called on every change', counter]);
results.push(['ensure refs are cleared', nodes.map(Boolean)]);
results.push(['ensure refs are different', new Set(nodes).size]);
return results;
};
module.exports = App;

View File

@ -0,0 +1,40 @@
var React = require('react');
this['React'] = React;
function Foo(props) {
return [[<div ref={props.callback} key="0" />]];
}
function Bar(props) {
return [<div ref={props.callback} key="0" />];
}
function App(props) {
return props.foo ? <Foo {...props} /> : <Bar {...props} />;
}
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App);
}
App.getTrials = function(renderer, Root) {
let counter = 0;
let nodes = [];
function callback(node) {
nodes.push(node);
counter++;
}
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={false} />);
renderer.update(<Root callback={callback} foo={false} />);
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={true} />);
let results = [];
results.push(['ensure ref is called on every change', counter]);
results.push(['ensure refs are cleared', nodes.map(Boolean)]);
results.push(['ensure refs are different', new Set(nodes).size]);
return results;
};
module.exports = App;

View File

@ -0,0 +1,40 @@
var React = require('react');
this['React'] = React;
function Foo(props) {
return [<div ref={props.callback} key="0" />];
}
function Bar(props) {
return props.bar ? [<div ref={props.callback} key="0" />] : [<div ref={props.callback} key="0" />];
}
function App(props) {
return props.foo ? <Foo {...props} /> : <Bar {...props} />;
}
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App);
}
App.getTrials = function(renderer, Root) {
let counter = 0;
let nodes = [];
function callback(node) {
nodes.push(node);
counter++;
}
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={false} />);
renderer.update(<Root callback={callback} foo={false} />);
renderer.update(<Root callback={callback} foo={true} />);
renderer.update(<Root callback={callback} foo={true} />);
let results = [];
results.push(['ensure ref is called on every change', counter]);
results.push(['ensure refs are cleared', nodes.map(Boolean)]);
results.push(['ensure refs are different', new Set(nodes).size]);
return results;
};
module.exports = App;

View File

@ -2100,6 +2100,606 @@ ReactStatistics {
}
`;
exports[`Key nesting 4: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 4: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 4: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 4: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 5: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 5: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 5: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 5: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 6: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 6: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 6: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 6: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 7: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 7: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 7: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 7: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 8: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 8: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 8: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting 8: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "Foo",
"status": "INLINED",
},
Object {
"children": Array [],
"message": "",
"name": "Bar",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 2,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Key nesting: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 6,