Skip to content

Commit 6498f62

Browse files
authored
Fix a mistake in ReactChildren refactor (#18380)
* Regression test for map() returning an array * Add forgotten argument This fixes the bug. * Remove unused arg and retval These aren't directly observable. The arg wasn't used, it's accidental and I forgot to remove. The retval was triggering a codepath that was unnecessary (pushing to array) so I removed that too. * Flowify ReactChildren * Tighten up types * Rename getComponentKey to getElementKey
1 parent 2ba43ed commit 6498f62

File tree

4 files changed

+133
-46
lines changed

4 files changed

+133
-46
lines changed

packages/react-dom/src/client/ReactDOMOption.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function flattenChildren(children) {
2525
if (child == null) {
2626
return;
2727
}
28-
content += child;
28+
content += (child: any);
2929
// Note: we don't warn about invalid children here.
3030
// Instead, this is done separately below so that
3131
// it happens during the hydration codepath too.
@@ -52,7 +52,7 @@ export function validateProps(element: Element, props: Object) {
5252
if (typeof child === 'string' || typeof child === 'number') {
5353
return;
5454
}
55-
if (typeof child.type !== 'string') {
55+
if (typeof (child: any).type !== 'string') {
5656
return;
5757
}
5858
if (!didWarnInvalidChild) {

packages/react-dom/src/server/ReactPartialRenderer.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,11 @@ function flattenOptionChildren(children: mixed): ?string {
315315
let content = '';
316316
// Flatten children and warn if they aren't strings or numbers;
317317
// invalid types are ignored.
318-
React.Children.forEach(children, function(child) {
318+
React.Children.forEach((children: any), function(child) {
319319
if (child == null) {
320320
return;
321321
}
322-
content += child;
322+
content += (child: any);
323323
if (__DEV__) {
324324
if (
325325
!didWarnInvalidOptionChildren &&

packages/react/src/ReactChildren.js

Lines changed: 61 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
*
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
68
*/
79

10+
import type {ReactNodeList} from 'shared/ReactTypes';
11+
812
import invariant from 'shared/invariant';
913
import {
1014
getIteratorFn,
@@ -25,13 +29,13 @@ const SUBSEPARATOR = ':';
2529
* @param {string} key to be escaped.
2630
* @return {string} the escaped key.
2731
*/
28-
function escape(key) {
32+
function escape(key: string): string {
2933
const escapeRegex = /[=:]/g;
3034
const escaperLookup = {
3135
'=': '=0',
3236
':': '=2',
3337
};
34-
const escapedString = ('' + key).replace(escapeRegex, function(match) {
38+
const escapedString = key.replace(escapeRegex, function(match) {
3539
return escaperLookup[match];
3640
});
3741

@@ -46,33 +50,35 @@ function escape(key) {
4650
let didWarnAboutMaps = false;
4751

4852
const userProvidedKeyEscapeRegex = /\/+/g;
49-
function escapeUserProvidedKey(text) {
50-
return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
53+
function escapeUserProvidedKey(text: string): string {
54+
return text.replace(userProvidedKeyEscapeRegex, '$&/');
5155
}
5256

5357
/**
54-
* Generate a key string that identifies a component within a set.
58+
* Generate a key string that identifies a element within a set.
5559
*
56-
* @param {*} component A component that could contain a manual key.
60+
* @param {*} element A element that could contain a manual key.
5761
* @param {number} index Index that is used if a manual key is not provided.
5862
* @return {string}
5963
*/
60-
function getComponentKey(component, index) {
64+
function getElementKey(element: any, index: number): string {
6165
// Do some typechecking here since we call this blindly. We want to ensure
6266
// that we don't block potential future ES APIs.
63-
if (
64-
typeof component === 'object' &&
65-
component !== null &&
66-
component.key != null
67-
) {
67+
if (typeof element === 'object' && element !== null && element.key != null) {
6868
// Explicit key
69-
return escape(component.key);
69+
return escape('' + element.key);
7070
}
7171
// Implicit key determined by the index in the set
7272
return index.toString(36);
7373
}
7474

75-
function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
75+
function mapIntoArray(
76+
children: ?ReactNodeList,
77+
array: Array<React$Node>,
78+
escapedPrefix: string,
79+
nameSoFar: string,
80+
callback: (?React$Node) => ?ReactNodeList,
81+
): number {
7682
const type = typeof children;
7783

7884
if (type === 'undefined' || type === 'boolean') {
@@ -91,7 +97,7 @@ function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
9197
invokeCallback = true;
9298
break;
9399
case 'object':
94-
switch (children.$$typeof) {
100+
switch ((children: any).$$typeof) {
95101
case REACT_ELEMENT_TYPE:
96102
case REACT_PORTAL_TYPE:
97103
invokeCallback = true;
@@ -105,22 +111,24 @@ function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
105111
// If it's the only child, treat the name as if it was wrapped in an array
106112
// so that it's consistent if the number of children grows:
107113
let childKey =
108-
nameSoFar === '' ? SEPARATOR + getComponentKey(child, 0) : nameSoFar;
114+
nameSoFar === '' ? SEPARATOR + getElementKey(child, 0) : nameSoFar;
109115
if (Array.isArray(mappedChild)) {
110116
let escapedChildKey = '';
111117
if (childKey != null) {
112118
escapedChildKey = escapeUserProvidedKey(childKey) + '/';
113119
}
114-
mapIntoArray(mappedChild, array, escapedChildKey, c => c);
120+
mapIntoArray(mappedChild, array, escapedChildKey, '', c => c);
115121
} else if (mappedChild != null) {
116122
if (isValidElement(mappedChild)) {
117123
mappedChild = cloneAndReplaceKey(
118124
mappedChild,
119125
// Keep both the (mapped) and old keys if they differ, just as
120126
// traverseAllChildren used to do for objects as children
121127
escapedPrefix +
128+
// $FlowFixMe Flow incorrectly thinks React.Portal doesn't have a key
122129
(mappedChild.key && (!child || child.key !== mappedChild.key)
123-
? escapeUserProvidedKey(mappedChild.key) + '/'
130+
? // $FlowFixMe Flow incorrectly thinks existing element's key can be a number
131+
escapeUserProvidedKey('' + mappedChild.key) + '/'
124132
: '') +
125133
childKey,
126134
);
@@ -139,7 +147,7 @@ function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
139147
if (Array.isArray(children)) {
140148
for (let i = 0; i < children.length; i++) {
141149
child = children[i];
142-
nextName = nextNamePrefix + getComponentKey(child, i);
150+
nextName = nextNamePrefix + getElementKey(child, i);
143151
subtreeCount += mapIntoArray(
144152
child,
145153
array,
@@ -151,18 +159,21 @@ function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
151159
} else {
152160
const iteratorFn = getIteratorFn(children);
153161
if (typeof iteratorFn === 'function') {
162+
const iterableChildren: Iterable<React$Node> & {
163+
entries: any,
164+
} = (children: any);
154165
if (disableMapsAsChildren) {
155166
invariant(
156-
iteratorFn !== children.entries,
167+
iteratorFn !== iterableChildren.entries,
157168
'Maps are not valid as a React child (found: %s). Consider converting ' +
158169
'children to an array of keyed ReactElements instead.',
159-
children,
170+
iterableChildren,
160171
);
161172
}
162173

163174
if (__DEV__) {
164175
// Warn about using Maps as children
165-
if (iteratorFn === children.entries) {
176+
if (iteratorFn === iterableChildren.entries) {
166177
if (!didWarnAboutMaps) {
167178
console.warn(
168179
'Using Maps as children is deprecated and will be removed in ' +
@@ -174,12 +185,12 @@ function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
174185
}
175186
}
176187

177-
const iterator = iteratorFn.call(children);
188+
const iterator = iteratorFn.call(iterableChildren);
178189
let step;
179190
let ii = 0;
180191
while (!(step = iterator.next()).done) {
181192
child = step.value;
182-
nextName = nextNamePrefix + getComponentKey(child, ii++);
193+
nextName = nextNamePrefix + getElementKey(child, ii++);
183194
subtreeCount += mapIntoArray(
184195
child,
185196
array,
@@ -196,12 +207,12 @@ function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
196207
'instead.' +
197208
ReactDebugCurrentFrame.getStackAddendum();
198209
}
199-
const childrenString = '' + children;
210+
const childrenString = '' + (children: any);
200211
invariant(
201212
false,
202213
'Objects are not valid as a React child (found: %s).%s',
203214
childrenString === '[object Object]'
204-
? 'object with keys {' + Object.keys(children).join(', ') + '}'
215+
? 'object with keys {' + Object.keys((children: any)).join(', ') + '}'
205216
: childrenString,
206217
addendum,
207218
);
@@ -211,6 +222,8 @@ function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
211222
return subtreeCount;
212223
}
213224

225+
type MapFunc = (child: ?React$Node) => ?ReactNodeList;
226+
214227
/**
215228
* Maps children that are typically specified as `props.children`.
216229
*
@@ -224,22 +237,19 @@ function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
224237
* @param {*} context Context for mapFunction.
225238
* @return {object} Object containing the ordered map of results.
226239
*/
227-
function mapChildren(children, func, context) {
240+
function mapChildren(
241+
children: ?ReactNodeList,
242+
func: MapFunc,
243+
context: mixed,
244+
): ?Array<React$Node> {
228245
if (children == null) {
229246
return children;
230247
}
231248
const result = [];
232249
let count = 0;
233-
mapIntoArray(
234-
children,
235-
result,
236-
'',
237-
'',
238-
function(child) {
239-
return func.call(context, child, count++);
240-
},
241-
context,
242-
);
250+
mapIntoArray(children, result, '', '', function(child) {
251+
return func.call(context, child, count++);
252+
});
243253
return result;
244254
}
245255

@@ -252,12 +262,17 @@ function mapChildren(children, func, context) {
252262
* @param {?*} children Children tree container.
253263
* @return {number} The number of children.
254264
*/
255-
function countChildren(children) {
265+
function countChildren(children: ?ReactNodeList): number {
256266
let n = 0;
257-
mapChildren(children, () => n++);
267+
mapChildren(children, () => {
268+
n++;
269+
// Don't return anything
270+
});
258271
return n;
259272
}
260273

274+
type ForEachFunc = (child: ?React$Node) => void;
275+
261276
/**
262277
* Iterates through children that are typically specified as `props.children`.
263278
*
@@ -270,7 +285,11 @@ function countChildren(children) {
270285
* @param {function(*, int)} forEachFunc
271286
* @param {*} forEachContext Context for forEachContext.
272287
*/
273-
function forEachChildren(children, forEachFunc, forEachContext) {
288+
function forEachChildren(
289+
children: ?ReactNodeList,
290+
forEachFunc: ForEachFunc,
291+
forEachContext: mixed,
292+
): void {
274293
mapChildren(
275294
children,
276295
function() {
@@ -287,7 +306,7 @@ function forEachChildren(children, forEachFunc, forEachContext) {
287306
*
288307
* See https://reactjs.org/docs/react-api.html#reactchildrentoarray
289308
*/
290-
function toArray(children) {
309+
function toArray(children: ?ReactNodeList): Array<React$Node> {
291310
return mapChildren(children, child => child) || [];
292311
}
293312

@@ -305,7 +324,7 @@ function toArray(children) {
305324
* @return {ReactElement} The first and only `ReactElement` contained in the
306325
* structure.
307326
*/
308-
function onlyChild(children) {
327+
function onlyChild<T>(children: T): T {
309328
invariant(
310329
isValidElement(children),
311330
'React.Children.only expected to receive a single React element child.',

packages/react/src/__tests__/ReactChildren-test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,74 @@ describe('ReactChildren', () => {
866866
]);
867867
});
868868

869+
it('should combine keys when map returns an array', () => {
870+
const instance = (
871+
<div>
872+
<div key="a" />
873+
{false}
874+
<div key="b" />
875+
<p />
876+
</div>
877+
);
878+
const mappedChildren = React.Children.map(
879+
instance.props.children,
880+
// Try a few things: keyed, unkeyed, hole, and a cloned element.
881+
kid => [
882+
<span key="x" />,
883+
null,
884+
<span key="y" />,
885+
kid,
886+
kid && React.cloneElement(kid, {key: 'z'}),
887+
<hr />,
888+
],
889+
);
890+
expect(mappedChildren.length).toBe(18);
891+
892+
// <div key="a">
893+
expect(mappedChildren[0].type).toBe('span');
894+
expect(mappedChildren[0].key).toBe('.$a/.$x');
895+
expect(mappedChildren[1].type).toBe('span');
896+
expect(mappedChildren[1].key).toBe('.$a/.$y');
897+
expect(mappedChildren[2].type).toBe('div');
898+
expect(mappedChildren[2].key).toBe('.$a/.$a');
899+
expect(mappedChildren[3].type).toBe('div');
900+
expect(mappedChildren[3].key).toBe('.$a/.$z');
901+
expect(mappedChildren[4].type).toBe('hr');
902+
expect(mappedChildren[4].key).toBe('.$a/.5');
903+
904+
// false
905+
expect(mappedChildren[5].type).toBe('span');
906+
expect(mappedChildren[5].key).toBe('.1/.$x');
907+
expect(mappedChildren[6].type).toBe('span');
908+
expect(mappedChildren[6].key).toBe('.1/.$y');
909+
expect(mappedChildren[7].type).toBe('hr');
910+
expect(mappedChildren[7].key).toBe('.1/.5');
911+
912+
// <div key="b">
913+
expect(mappedChildren[8].type).toBe('span');
914+
expect(mappedChildren[8].key).toBe('.$b/.$x');
915+
expect(mappedChildren[9].type).toBe('span');
916+
expect(mappedChildren[9].key).toBe('.$b/.$y');
917+
expect(mappedChildren[10].type).toBe('div');
918+
expect(mappedChildren[10].key).toBe('.$b/.$b');
919+
expect(mappedChildren[11].type).toBe('div');
920+
expect(mappedChildren[11].key).toBe('.$b/.$z');
921+
expect(mappedChildren[12].type).toBe('hr');
922+
expect(mappedChildren[12].key).toBe('.$b/.5');
923+
924+
// <p>
925+
expect(mappedChildren[13].type).toBe('span');
926+
expect(mappedChildren[13].key).toBe('.3/.$x');
927+
expect(mappedChildren[14].type).toBe('span');
928+
expect(mappedChildren[14].key).toBe('.3/.$y');
929+
expect(mappedChildren[15].type).toBe('p');
930+
expect(mappedChildren[15].key).toBe('.3/.3');
931+
expect(mappedChildren[16].type).toBe('p');
932+
expect(mappedChildren[16].key).toBe('.3/.$z');
933+
expect(mappedChildren[17].type).toBe('hr');
934+
expect(mappedChildren[17].key).toBe('.3/.5');
935+
});
936+
869937
it('should throw on object', () => {
870938
expect(function() {
871939
React.Children.forEach({a: 1, b: 2}, function() {}, null);

0 commit comments

Comments
 (0)