mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Respect 'key' prop for object identity
Now when a `key` prop appears, its value is always honored. This means that in the root component or as an only child, changing key will cause remounting; in a `children` object, the `key` prop will be joined with the object key to form a two-part key. Fixes #590.
This commit is contained in:
@@ -155,23 +155,6 @@ var ReactComponent = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a key string that identifies a component within a set.
|
|
||||||
*
|
|
||||||
* @param {*} component A component that could contain a manual key.
|
|
||||||
* @param {number} index Index that is used if a manual key is not provided.
|
|
||||||
* @return {string}
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
getKey: function(component, index) {
|
|
||||||
if (component && component.props && component.props.key != null) {
|
|
||||||
// Explicit key
|
|
||||||
return '{' + component.props.key + '}';
|
|
||||||
}
|
|
||||||
// Implicit key determined by the index in the set
|
|
||||||
return '[' + index + ']';
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ describe('ReactIdentity', function() {
|
|||||||
React.renderComponent(instance, document.createElement('div'));
|
React.renderComponent(instance, document.createElement('div'));
|
||||||
var node = instance.getDOMNode();
|
var node = instance.getDOMNode();
|
||||||
reactComponentExpect(instance).toBeDOMComponentWithChildCount(2);
|
reactComponentExpect(instance).toBeDOMComponentWithChildCount(2);
|
||||||
checkId(node.childNodes[0], '.r[0].{first}');
|
checkId(node.childNodes[0], '.r[0].{first}[0]');
|
||||||
checkId(node.childNodes[1], '.r[0].{second}');
|
checkId(node.childNodes[1], '.r[0].{second}[0]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow key property to express identity', function() {
|
it('should allow key property to express identity', function() {
|
||||||
@@ -117,7 +117,7 @@ describe('ReactIdentity', function() {
|
|||||||
expect(span2.getDOMNode()).not.toBe(null);
|
expect(span2.getDOMNode()).not.toBe(null);
|
||||||
|
|
||||||
checkId(span1.getDOMNode(), '.r[0].{' + key + '}');
|
checkId(span1.getDOMNode(), '.r[0].{' + key + '}');
|
||||||
checkId(span2.getDOMNode(), '.r[0].[1]{' + key + '}');
|
checkId(span2.getDOMNode(), '.r[0].[1]{' + key + '}[0]');
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should allow any character as a key, in a detached parent', function() {
|
it('should allow any character as a key, in a detached parent', function() {
|
||||||
|
|||||||
@@ -35,4 +35,39 @@ describe('ReactMount', function() {
|
|||||||
ReactMount.renderComponent(<span></span>, container);
|
ReactMount.renderComponent(<span></span>, container);
|
||||||
expect(container.firstChild.nodeName).toBe('SPAN');
|
expect(container.firstChild.nodeName).toBe('SPAN');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should unmount and remount if the key changes', function() {
|
||||||
|
var container = document.createElement('container');
|
||||||
|
|
||||||
|
var mockMount = mocks.getMockFunction();
|
||||||
|
var mockUnmount = mocks.getMockFunction();
|
||||||
|
|
||||||
|
var Component = React.createClass({
|
||||||
|
componentDidMount: mockMount,
|
||||||
|
componentWillUnmount: mockUnmount,
|
||||||
|
render: function() {
|
||||||
|
return <span>{this.props.text}</span>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockMount.mock.calls.length).toBe(0);
|
||||||
|
expect(mockUnmount.mock.calls.length).toBe(0);
|
||||||
|
|
||||||
|
ReactMount.renderComponent(<Component text="orange" key="A" />, container);
|
||||||
|
expect(container.firstChild.innerText).toBe('orange');
|
||||||
|
expect(mockMount.mock.calls.length).toBe(1);
|
||||||
|
expect(mockUnmount.mock.calls.length).toBe(0);
|
||||||
|
|
||||||
|
// If we change the key, the component is unmounted and remounted
|
||||||
|
ReactMount.renderComponent(<Component text="green" key="B" />, container);
|
||||||
|
expect(container.firstChild.innerText).toBe('green');
|
||||||
|
expect(mockMount.mock.calls.length).toBe(2);
|
||||||
|
expect(mockUnmount.mock.calls.length).toBe(1);
|
||||||
|
|
||||||
|
// But if we don't change the key, the component instance is reused
|
||||||
|
ReactMount.renderComponent(<Component text="blue" key="B" />, container);
|
||||||
|
expect(container.firstChild.innerText).toBe('blue');
|
||||||
|
expect(mockMount.mock.calls.length).toBe(2);
|
||||||
|
expect(mockUnmount.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,6 +125,34 @@ describe('ReactMultiChild', function() {
|
|||||||
expect(mockMount.mock.calls.length).toBe(2);
|
expect(mockMount.mock.calls.length).toBe(2);
|
||||||
expect(mockUnmount.mock.calls.length).toBe(1);
|
expect(mockUnmount.mock.calls.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should replace children with different keys', function() {
|
||||||
|
var container = document.createElement('div');
|
||||||
|
|
||||||
|
var mockMount = mocks.getMockFunction();
|
||||||
|
var mockUnmount = mocks.getMockFunction();
|
||||||
|
|
||||||
|
var MockComponent = React.createClass({
|
||||||
|
componentDidMount: mockMount,
|
||||||
|
componentWillUnmount: mockUnmount,
|
||||||
|
render: function() {
|
||||||
|
return <span />;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockMount.mock.calls.length).toBe(0);
|
||||||
|
expect(mockUnmount.mock.calls.length).toBe(0);
|
||||||
|
|
||||||
|
React.renderComponent(<div><MockComponent key="A" /></div>, container);
|
||||||
|
|
||||||
|
expect(mockMount.mock.calls.length).toBe(1);
|
||||||
|
expect(mockUnmount.mock.calls.length).toBe(0);
|
||||||
|
|
||||||
|
React.renderComponent(<div><MockComponent key="B" /></div>, container);
|
||||||
|
|
||||||
|
expect(mockMount.mock.calls.length).toBe(2);
|
||||||
|
expect(mockUnmount.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('innerHTML', function() {
|
describe('innerHTML', function() {
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ var stripEmptyValues = function(obj) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Child names is are wrapped in an prefix and suffix character. We strip those
|
* Child key names are wrapped like '{key}[0]'. We strip the extra chars out
|
||||||
* out. This relies on a tiny implementation detail of the rendering system.
|
* here. This relies on an implementation detail of the rendering system.
|
||||||
*/
|
*/
|
||||||
var getOriginalKey = function(childName) {
|
var getOriginalKey = function(childName) {
|
||||||
return childName.substr(1, childName.length - 2);
|
return childName.slice('{'.length, childName.length - '}[0]'.length);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013 Facebook, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @providesModule getComponentKey
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a key string that identifies a component within a set.
|
||||||
|
*
|
||||||
|
* @param {*} component A component that could contain a manual key.
|
||||||
|
* @param {number} index Index that is used if a manual key is not provided.
|
||||||
|
* @return {string}
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
function getComponentKey(component, index) {
|
||||||
|
if (component && component.props && component.props.key != null) {
|
||||||
|
// Explicit key
|
||||||
|
return '{' + component.props.key + '}';
|
||||||
|
}
|
||||||
|
// Implicit key determined by the index in the set
|
||||||
|
return '[' + index + ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getComponentKey;
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
var getComponentKey = require('getComponentKey');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a `prevComponent` and `nextComponent`, determines if `prevComponent`
|
* Given a `prevComponent` and `nextComponent`, determines if `prevComponent`
|
||||||
* should be updated as opposed to being destroyed or replaced.
|
* should be updated as opposed to being destroyed or replaced.
|
||||||
@@ -32,7 +34,10 @@ function shouldUpdateReactComponent(prevComponent, nextComponent) {
|
|||||||
// TODO: Remove warning after a release.
|
// TODO: Remove warning after a release.
|
||||||
if (prevComponent && nextComponent &&
|
if (prevComponent && nextComponent &&
|
||||||
prevComponent.constructor === nextComponent.constructor) {
|
prevComponent.constructor === nextComponent.constructor) {
|
||||||
if (prevComponent._owner === nextComponent._owner) {
|
if (prevComponent._owner === nextComponent._owner && (
|
||||||
|
getComponentKey(prevComponent, /* index: */ 0) ===
|
||||||
|
getComponentKey(nextComponent, /* index: */ 0)
|
||||||
|
)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ describe('ReactChildren', function() {
|
|||||||
var three = null;
|
var three = null;
|
||||||
var four = <div key="keyFour" />;
|
var four = <div key="keyFour" />;
|
||||||
|
|
||||||
var zeroMapped = <div key="giraffe" />; // Key should be overridden
|
var zeroMapped = <div key="giraffe" />; // Key should be joined to obj key
|
||||||
var oneMapped = null; // Key should be added even if we don't supply it!
|
var oneMapped = null; // Key should be added even if we don't supply it!
|
||||||
var twoMapped = <div />; // Key should be added even if not supplied!
|
var twoMapped = <div />; // Key should be added even if not supplied!
|
||||||
var threeMapped = <span />; // Map from null to something.
|
var threeMapped = <span />; // Map from null to something.
|
||||||
@@ -188,13 +188,12 @@ describe('ReactChildren', function() {
|
|||||||
var two = <div key="keyTwo" />;
|
var two = <div key="keyTwo" />;
|
||||||
var three = null;
|
var three = null;
|
||||||
var four = <div key="keyFour" />;
|
var four = <div key="keyFour" />;
|
||||||
var five = <div key="keyFiveCompletelyIgnored" />;
|
var five = <div key="keyFiveInner" />;
|
||||||
// five is placed into a JS object with a key that takes precedence over the
|
// five is placed into a JS object with a key that is joined to the
|
||||||
// component key attribute.
|
// component key attribute.
|
||||||
// Precedence is as follows:
|
// Precedence is as follows:
|
||||||
// 1. JavaScript Object key if in a JavaScript object:
|
// 1. If grouped in an Object, the object key combined with `key` prop
|
||||||
// 2. If grouped in an Array, the `key` attribute.
|
// 2. If grouped in an Array, the `key` prop, falling back to array index
|
||||||
// 3. The array index if in a JavaScript Array.
|
|
||||||
|
|
||||||
var zeroMapped = <div key="giraffe" />; // Key should be overridden
|
var zeroMapped = <div key="giraffe" />; // Key should be overridden
|
||||||
var oneMapped = null; // Key should be added even if we don't supply it!
|
var oneMapped = null; // Key should be added even if we don't supply it!
|
||||||
@@ -236,12 +235,12 @@ describe('ReactChildren', function() {
|
|||||||
expect(mappedKeys.length).toBe(6);
|
expect(mappedKeys.length).toBe(6);
|
||||||
// Keys default to indices.
|
// Keys default to indices.
|
||||||
expect(mappedKeys).toEqual([
|
expect(mappedKeys).toEqual([
|
||||||
'[0]{firstHalfKey}{keyZero}',
|
'[0]{firstHalfKey}[0]{keyZero}',
|
||||||
'[0]{firstHalfKey}[1]',
|
'[0]{firstHalfKey}[0][1]',
|
||||||
'[0]{firstHalfKey}{keyTwo}',
|
'[0]{firstHalfKey}[0]{keyTwo}',
|
||||||
'[0]{secondHalfKey}[0]',
|
'[0]{secondHalfKey}[0][0]',
|
||||||
'[0]{secondHalfKey}{keyFour}',
|
'[0]{secondHalfKey}[0]{keyFour}',
|
||||||
'[0]{keyFive}'
|
'[0]{keyFive}{keyFiveInner}'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalledWith(zero, 0);
|
expect(callback).toHaveBeenCalledWith(zero, 0);
|
||||||
@@ -267,7 +266,7 @@ describe('ReactChildren', function() {
|
|||||||
var zeroForceKey = <div key="keyZero" />;
|
var zeroForceKey = <div key="keyZero" />;
|
||||||
var oneForceKey = <div key="keyOne" />;
|
var oneForceKey = <div key="keyOne" />;
|
||||||
|
|
||||||
// Key should be overridden
|
// Key should be joined to object key
|
||||||
var zeroForceKeyMapped = <div key="giraffe" />;
|
var zeroForceKeyMapped = <div key="giraffe" />;
|
||||||
// Key should be added even if we don't supply it!
|
// Key should be added even if we don't supply it!
|
||||||
var oneForceKeyMapped = <div />;
|
var oneForceKeyMapped = <div />;
|
||||||
@@ -289,7 +288,7 @@ describe('ReactChildren', function() {
|
|||||||
var mappedForcedKeys = Object.keys(mappedChildrenForcedKeys);
|
var mappedForcedKeys = Object.keys(mappedChildrenForcedKeys);
|
||||||
expect(mappedForcedKeys).toEqual(expectedForcedKeys);
|
expect(mappedForcedKeys).toEqual(expectedForcedKeys);
|
||||||
|
|
||||||
var expectedRemappedForcedKeys = ['{{keyZero}}', '{{keyOne}}'];
|
var expectedRemappedForcedKeys = ['{{keyZero}}{giraffe}', '{{keyOne}}[0]'];
|
||||||
var remappedChildrenForcedKeys =
|
var remappedChildrenForcedKeys =
|
||||||
ReactChildren.map(mappedChildrenForcedKeys, mapFn);
|
ReactChildren.map(mappedChildrenForcedKeys, mapFn);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -141,11 +141,12 @@ describe('traverseAllChildren', function() {
|
|||||||
var two = <div key="keyTwo" />;
|
var two = <div key="keyTwo" />;
|
||||||
var three = null;
|
var three = null;
|
||||||
var four = <div key="keyFour" />;
|
var four = <div key="keyFour" />;
|
||||||
var five = <div key="keyFiveCompletelyIgnored" />;
|
var five = <div key="keyFiveInner" />;
|
||||||
// Name precedence is as follows:
|
// five is placed into a JS object with a key that is joined to the
|
||||||
// 1. JavaScript Object key if in a JavaScript object:
|
// component key attribute.
|
||||||
// 2. If grouped in an Array, the `key` attribute.
|
// Precedence is as follows:
|
||||||
// 3. The array index if in a JavaScript Array.
|
// 1. If grouped in an Object, the object key combined with `key` prop
|
||||||
|
// 2. If grouped in an Array, the `key` prop, falling back to array index
|
||||||
|
|
||||||
|
|
||||||
var traverseContext = [];
|
var traverseContext = [];
|
||||||
@@ -170,32 +171,40 @@ describe('traverseAllChildren', function() {
|
|||||||
expect(traverseFn).toHaveBeenCalledWith(
|
expect(traverseFn).toHaveBeenCalledWith(
|
||||||
traverseContext,
|
traverseContext,
|
||||||
zero,
|
zero,
|
||||||
'[0]{firstHalfKey}{keyZero}',
|
'[0]{firstHalfKey}[0]{keyZero}',
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(traverseFn)
|
expect(traverseFn)
|
||||||
.toHaveBeenCalledWith(traverseContext, one, '[0]{firstHalfKey}[1]', 1);
|
.toHaveBeenCalledWith(traverseContext, one, '[0]{firstHalfKey}[0][1]', 1);
|
||||||
|
|
||||||
expect(traverseFn).toHaveBeenCalledWith(
|
expect(traverseFn).toHaveBeenCalledWith(
|
||||||
traverseContext,
|
traverseContext,
|
||||||
two,
|
two,
|
||||||
'[0]{firstHalfKey}{keyTwo}',
|
'[0]{firstHalfKey}[0]{keyTwo}',
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(traverseFn)
|
expect(traverseFn).toHaveBeenCalledWith(
|
||||||
.toHaveBeenCalledWith(traverseContext, three, '[0]{secondHalfKey}[0]', 3);
|
traverseContext,
|
||||||
|
three,
|
||||||
|
'[0]{secondHalfKey}[0][0]',
|
||||||
|
3
|
||||||
|
);
|
||||||
|
|
||||||
expect(traverseFn).toHaveBeenCalledWith(
|
expect(traverseFn).toHaveBeenCalledWith(
|
||||||
traverseContext,
|
traverseContext,
|
||||||
four,
|
four,
|
||||||
'[0]{secondHalfKey}{keyFour}',
|
'[0]{secondHalfKey}[0]{keyFour}',
|
||||||
4
|
4
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(traverseFn)
|
expect(traverseFn).toHaveBeenCalledWith(
|
||||||
.toHaveBeenCalledWith(traverseContext, five, '[0]{keyFive}', 5);
|
traverseContext,
|
||||||
|
five,
|
||||||
|
'[0]{keyFive}{keyFiveInner}',
|
||||||
|
5
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should retain key across two mappings', function() {
|
it('should retain key across two mappings', function() {
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var ReactComponent = require('ReactComponent');
|
|
||||||
var ReactTextComponent = require('ReactTextComponent');
|
var ReactTextComponent = require('ReactTextComponent');
|
||||||
|
|
||||||
|
var getComponentKey = require('getComponentKey');
|
||||||
var invariant = require('invariant');
|
var invariant = require('invariant');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,7 +46,7 @@ var traverseAllChildrenImpl =
|
|||||||
if (Array.isArray(children)) {
|
if (Array.isArray(children)) {
|
||||||
for (var i = 0; i < children.length; i++) {
|
for (var i = 0; i < children.length; i++) {
|
||||||
var child = children[i];
|
var child = children[i];
|
||||||
var nextName = nameSoFar + ReactComponent.getKey(child, i);
|
var nextName = nameSoFar + getComponentKey(child, i);
|
||||||
var nextIndex = indexSoFar + subtreeCount;
|
var nextIndex = indexSoFar + subtreeCount;
|
||||||
subtreeCount += traverseAllChildrenImpl(
|
subtreeCount += traverseAllChildrenImpl(
|
||||||
child,
|
child,
|
||||||
@@ -61,9 +61,7 @@ var traverseAllChildrenImpl =
|
|||||||
var isOnlyChild = nameSoFar === '';
|
var isOnlyChild = nameSoFar === '';
|
||||||
// If it's the only child, treat the name as if it was wrapped in an array
|
// If it's the only child, treat the name as if it was wrapped in an array
|
||||||
// so that it's consistent if the number of children grows
|
// so that it's consistent if the number of children grows
|
||||||
var storageName = isOnlyChild ?
|
var storageName = isOnlyChild ? getComponentKey(children, 0) : nameSoFar;
|
||||||
ReactComponent.getKey(children, 0):
|
|
||||||
nameSoFar;
|
|
||||||
if (children === null || children === undefined || type === 'boolean') {
|
if (children === null || children === undefined || type === 'boolean') {
|
||||||
// All of the above are perceived as null.
|
// All of the above are perceived as null.
|
||||||
callback(traverseContext, null, storageName, indexSoFar);
|
callback(traverseContext, null, storageName, indexSoFar);
|
||||||
@@ -82,7 +80,7 @@ var traverseAllChildrenImpl =
|
|||||||
if (children.hasOwnProperty(key)) {
|
if (children.hasOwnProperty(key)) {
|
||||||
subtreeCount += traverseAllChildrenImpl(
|
subtreeCount += traverseAllChildrenImpl(
|
||||||
children[key],
|
children[key],
|
||||||
nameSoFar + '{' + key + '}',
|
nameSoFar + '{' + key + '}' + getComponentKey(children[key], 0),
|
||||||
indexSoFar + subtreeCount,
|
indexSoFar + subtreeCount,
|
||||||
callback,
|
callback,
|
||||||
traverseContext
|
traverseContext
|
||||||
|
|||||||
Reference in New Issue
Block a user