| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | * |
| 7 | * @flow |
| 8 | */ |
| 9 | |
| 10 | import type { |
| 11 | ReactNodeList, |
| 12 | Thenable, |
| 13 | PendingThenable, |
| 14 | FulfilledThenable, |
| 15 | RejectedThenable, |
| 16 | } from 'shared/ReactTypes'; |
| 17 | |
| 18 | import isArray from 'shared/isArray'; |
| 19 | import noop from 'shared/noop'; |
| 20 | import { |
| 21 | getIteratorFn, |
| 22 | REACT_ELEMENT_TYPE, |
| 23 | REACT_LAZY_TYPE, |
| 24 | REACT_PORTAL_TYPE, |
| 25 | REACT_OPTIMISTIC_KEY, |
| 26 | } from 'shared/ReactSymbols'; |
| 27 | import {enableOptimisticKey} from 'shared/ReactFeatureFlags'; |
| 28 | import {checkKeyStringCoercion} from 'shared/CheckStringCoercion'; |
| 29 | |
| 30 | import {isValidElement, cloneAndReplaceKey} from './jsx/ReactJSXElement'; |
| 31 | |
| 32 | const SEPARATOR = '.'; |
| 33 | const SUBSEPARATOR = ':'; |
| 34 | |
| 35 | /** |
| 36 | * Escape and wrap key so it is safe to use as a reactid |
| 37 | * |
| 38 | * @param {string} key to be escaped. |
| 39 | * @return {string} the escaped key. |
| 40 | */ |
| 41 | function escape(key: string): string { |
| 42 | const escapeRegex = /[=:]/g; |
| 43 | const escaperLookup = { |
| 44 | '=': '=0', |
| 45 | ':': '=2', |
| 46 | }; |
| 47 | const escapedString = key.replace(escapeRegex, function (match) { |
| 48 | // $FlowFixMe[invalid-computed-prop] |
| 49 | return escaperLookup[match]; |
| 50 | }); |
| 51 | |
| 52 | return '$' + escapedString; |
| 53 | } |
| 54 | |
| 55 | /** |
| 56 | * TODO: Test that a single child and an array with one item have the same key |
| 57 | * pattern. |
| 58 | */ |
| 59 | |
| 60 | let didWarnAboutMaps = false; |
| 61 | |
| 62 | const userProvidedKeyEscapeRegex = /\/+/g; |
| 63 | function escapeUserProvidedKey(text: string): string { |
| 64 | return text.replace(userProvidedKeyEscapeRegex, '$&/'); |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Generate a key string that identifies a element within a set. |
| 69 | * |
| 70 | * @param {*} element A element that could contain a manual key. |
| 71 | * @param {number} index Index that is used if a manual key is not provided. |
| 72 | * @return {string} |
| 73 | */ |
| 74 | function getElementKey(element: any, index: number): string { |
| 75 | // Do some typechecking here since we call this blindly. We want to ensure |
| 76 | // that we don't block potential future ES APIs. |
| 77 | if (typeof element === 'object' && element !== null && element.key != null) { |
| 78 | if (enableOptimisticKey && element.key === REACT_OPTIMISTIC_KEY) { |
| 79 | // For React.Children purposes this is treated as just null. |
| 80 | if (__DEV__) { |
| 81 | console.error("React.Children helpers don't support optimisticKey."); |
| 82 | } |
| 83 | return index.toString(36); |
| 84 | } |
| 85 | // Explicit key |
| 86 | if (__DEV__) { |
| 87 | checkKeyStringCoercion(element.key); |
| 88 | } |
| 89 | return escape('' + element.key); |
| 90 | } |
| 91 | // Implicit key determined by the index in the set |
| 92 | return index.toString(36); |
| 93 | } |
| 94 | |
| 95 | function resolveThenable<T>(thenable: Thenable<T>): T { |
| 96 | switch (thenable.status) { |
| 97 | case 'fulfilled': { |
| 98 | const fulfilledValue: T = thenable.value; |
| 99 | return fulfilledValue; |
| 100 | } |
| 101 | case 'rejected': { |
| 102 | const rejectedError = thenable.reason; |
| 103 | throw rejectedError; |
| 104 | } |
| 105 | default: { |
| 106 | if (typeof thenable.status === 'string') { |
| 107 | // Only instrument the thenable if the status if not defined. If |
| 108 | // it's defined, but an unknown value, assume it's been instrumented by |
| 109 | // some custom userspace implementation. We treat it as "pending". |
| 110 | // Attach a dummy listener, to ensure that any lazy initialization can |
| 111 | // happen. Flight lazily parses JSON when the value is actually awaited. |
| 112 | thenable.then(noop, noop); |
| 113 | } else { |
| 114 | // This is an uncached thenable that we haven't seen before. |
| 115 | |
| 116 | // TODO: Detect infinite ping loops caused by uncached promises. |
| 117 | |
| 118 | const pendingThenable: PendingThenable<T> = thenable as any; |
| 119 | pendingThenable.status = 'pending'; |
| 120 | pendingThenable.then( |
| 121 | fulfilledValue => { |
| 122 | if (thenable.status === 'pending') { |
| 123 | const fulfilledThenable: FulfilledThenable<T> = thenable as any; |
| 124 | fulfilledThenable.status = 'fulfilled'; |
| 125 | fulfilledThenable.value = fulfilledValue; |
| 126 | } |
| 127 | }, |
| 128 | (error: mixed) => { |
| 129 | if (thenable.status === 'pending') { |
| 130 | const rejectedThenable: RejectedThenable<T> = thenable as any; |
| 131 | rejectedThenable.status = 'rejected'; |
| 132 | rejectedThenable.reason = error; |
| 133 | } |
| 134 | }, |
| 135 | ); |
| 136 | } |
| 137 | |
| 138 | // Check one more time in case the thenable resolved synchronously. |
| 139 | switch ((thenable as Thenable<T>).status) { |
| 140 | case 'fulfilled': { |
| 141 | const fulfilledThenable: FulfilledThenable<T> = thenable as any; |
| 142 | return fulfilledThenable.value; |
| 143 | } |
| 144 | case 'rejected': { |
| 145 | const rejectedThenable: RejectedThenable<T> = thenable as any; |
| 146 | const rejectedError = rejectedThenable.reason; |
| 147 | throw rejectedError; |
| 148 | } |
| 149 | } |
| 150 | } |
| 151 | } |
| 152 | throw thenable; |
| 153 | } |
| 154 | |
| 155 | function mapIntoArray( |
| 156 | children: ?ReactNodeList, |
| 157 | array: Array<React$Node>, |
| 158 | escapedPrefix: string, |
| 159 | nameSoFar: string, |
| 160 | callback: (?React$Node) => ?ReactNodeList, |
| 161 | ): number { |
| 162 | const type = typeof children; |
| 163 | |
| 164 | if (type === 'undefined' || type === 'boolean') { |
| 165 | // All of the above are perceived as null. |
| 166 | children = null; |
| 167 | } |
| 168 | |
| 169 | let invokeCallback = false; |
| 170 | |
| 171 | if (children === null) { |
| 172 | invokeCallback = true; |
| 173 | } else { |
| 174 | switch (type) { |
| 175 | case 'bigint': |
| 176 | case 'string': |
| 177 | case 'number': |
| 178 | invokeCallback = true; |
| 179 | break; |
| 180 | case 'object': |
| 181 | switch ((children as any).$$typeof) { |
| 182 | case REACT_ELEMENT_TYPE: |
| 183 | case REACT_PORTAL_TYPE: |
| 184 | invokeCallback = true; |
| 185 | break; |
| 186 | case REACT_LAZY_TYPE: |
| 187 | const payload = (children as any)._payload; |
| 188 | const init = (children as any)._init; |
| 189 | return mapIntoArray( |
| 190 | init(payload), |
| 191 | array, |
| 192 | escapedPrefix, |
| 193 | nameSoFar, |
| 194 | callback, |
| 195 | ); |
| 196 | } |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | if (invokeCallback) { |
| 201 | const child = children; |
| 202 | let mappedChild = callback(child); |
| 203 | // If it's the only child, treat the name as if it was wrapped in an array |
| 204 | // so that it's consistent if the number of children grows: |
| 205 | const childKey = |
| 206 | nameSoFar === '' ? SEPARATOR + getElementKey(child, 0) : nameSoFar; |
| 207 | if (isArray(mappedChild)) { |
| 208 | let escapedChildKey = ''; |
| 209 | if (childKey != null) { |
| 210 | escapedChildKey = escapeUserProvidedKey(childKey) + '/'; |
| 211 | } |
| 212 | mapIntoArray(mappedChild, array, escapedChildKey, '', c => c); |
| 213 | } else if (mappedChild != null) { |
| 214 | if (isValidElement(mappedChild)) { |
| 215 | if (__DEV__) { |
| 216 | // The `if` statement here prevents auto-disabling of the safe |
| 217 | // coercion ESLint rule, so we must manually disable it below. |
| 218 | // $FlowFixMe[incompatible-type] Flow incorrectly thinks React.Portal doesn't have a key |
| 219 | if (mappedChild.key != null) { |
| 220 | if (!child || child.key !== mappedChild.key) { |
| 221 | checkKeyStringCoercion(mappedChild.key); |
| 222 | } |
| 223 | } |
| 224 | } |
| 225 | const newChild = cloneAndReplaceKey( |
| 226 | mappedChild, |
| 227 | // Keep both the (mapped) and old keys if they differ, just as |
| 228 | // traverseAllChildren used to do for objects as children |
| 229 | escapedPrefix + |
| 230 | // $FlowFixMe[incompatible-type] Flow incorrectly thinks React.Portal doesn't have a key |
| 231 | (mappedChild.key != null && |
| 232 | (!child || child.key !== mappedChild.key) |
| 233 | ? escapeUserProvidedKey( |
| 234 | // $FlowFixMe[unsafe-addition] |
| 235 | '' + mappedChild.key, // eslint-disable-line react-internal/safe-string-coercion |
| 236 | ) + '/' |
| 237 | : '') + |
| 238 | childKey, |
| 239 | ); |
| 240 | if (__DEV__) { |
| 241 | // If `child` was an element without a `key`, we need to validate if |
| 242 | // it should have had a `key`, before assigning one to `mappedChild`. |
| 243 | // $FlowFixMe[incompatible-type] Flow incorrectly thinks React.Portal doesn't have a key |
| 244 | if ( |
| 245 | nameSoFar !== '' && |
| 246 | child != null && |
| 247 | isValidElement(child) && |
| 248 | child.key == null |
| 249 | ) { |
| 250 | // We check truthiness of `child._store.validated` instead of being |
| 251 | // inequal to `1` to provide a bit of backward compatibility for any |
| 252 | // libraries (like `fbt`) which may be hacking this property. |
| 253 | if (child._store && !child._store.validated) { |
| 254 | // Mark this child as having failed validation, but let the actual |
| 255 | // renderer print the warning later. |
| 256 | newChild._store.validated = 2; |
| 257 | } |
| 258 | } |
| 259 | } |
| 260 | mappedChild = newChild; |
| 261 | } |
| 262 | array.push(mappedChild); |
| 263 | } |
| 264 | return 1; |
| 265 | } |
| 266 | |
| 267 | let child; |
| 268 | let nextName; |
| 269 | let subtreeCount = 0; // Count of children found in the current subtree. |
| 270 | const nextNamePrefix = |
| 271 | nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR; |
| 272 | |
| 273 | if (isArray(children)) { |
| 274 | for (let i = 0; i < children.length; i++) { |
| 275 | child = children[i]; |
| 276 | nextName = nextNamePrefix + getElementKey(child, i); |
| 277 | subtreeCount += mapIntoArray( |
| 278 | child, |
| 279 | array, |
| 280 | escapedPrefix, |
| 281 | nextName, |
| 282 | callback, |
| 283 | ); |
| 284 | } |
| 285 | } else { |
| 286 | const iteratorFn = getIteratorFn(children); |
| 287 | if (typeof iteratorFn === 'function') { |
| 288 | const iterableChildren: Iterable<React$Node> & { |
| 289 | entries: any, |
| 290 | } = children as any; |
| 291 | |
| 292 | if (__DEV__) { |
| 293 | // Warn about using Maps as children |
| 294 | if (iteratorFn === iterableChildren.entries) { |
| 295 | if (!didWarnAboutMaps) { |
| 296 | console.warn( |
| 297 | 'Using Maps as children is not supported. ' + |
| 298 | 'Use an array of keyed ReactElements instead.', |
| 299 | ); |
| 300 | } |
| 301 | didWarnAboutMaps = true; |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | const iterator = iteratorFn.call(iterableChildren); |
| 306 | let step; |
| 307 | let ii = 0; |
| 308 | // $FlowFixMe[incompatible-use] `iteratorFn` might return null according to typing. |
| 309 | while (!(step = iterator.next()).done) { |
| 310 | child = step.value; |
| 311 | nextName = nextNamePrefix + getElementKey(child, ii++); |
| 312 | subtreeCount += mapIntoArray( |
| 313 | child, |
| 314 | array, |
| 315 | escapedPrefix, |
| 316 | nextName, |
| 317 | callback, |
| 318 | ); |
| 319 | } |
| 320 | } else if (type === 'object') { |
| 321 | if (typeof (children as any).then === 'function') { |
| 322 | return mapIntoArray( |
| 323 | resolveThenable(children as any), |
| 324 | array, |
| 325 | escapedPrefix, |
| 326 | nameSoFar, |
| 327 | callback, |
| 328 | ); |
| 329 | } |
| 330 | |
| 331 | // eslint-disable-next-line react-internal/safe-string-coercion |
| 332 | const childrenString = String(children as any); |
| 333 | |
| 334 | throw new Error( |
| 335 | `Objects are not valid as a React child (found: ${ |
| 336 | childrenString === '[object Object]' |
| 337 | ? 'object with keys {' + |
| 338 | Object.keys(children as any).join(', ') + |
| 339 | '}' |
| 340 | : childrenString |
| 341 | }). ` + |
| 342 | 'If you meant to render a collection of children, use an array ' + |
| 343 | 'instead.', |
| 344 | ); |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | return subtreeCount; |
| 349 | } |
| 350 | |
| 351 | type MapFunc = (child: ?React$Node, index: number) => ?ReactNodeList; |
| 352 | |
| 353 | /** |
| 354 | * Maps children that are typically specified as `props.children`. |
| 355 | * |
| 356 | * See https://reactjs.org/docs/react-api.html#reactchildrenmap |
| 357 | * |
| 358 | * The provided mapFunction(child, index) will be called for each |
| 359 | * leaf child. |
| 360 | * |
| 361 | * @param {?*} children Children tree container. |
| 362 | * @param {function(*, int)} func The map function. |
| 363 | * @param {*} context Context for mapFunction. |
| 364 | * @return {object} Object containing the ordered map of results. |
| 365 | */ |
| 366 | function mapChildren( |
| 367 | children: ?ReactNodeList, |
| 368 | func: MapFunc, |
| 369 | context: mixed, |
| 370 | ): ?Array<React$Node> { |
| 371 | if (children == null) { |
| 372 | // $FlowFixMe[incompatible-type] limitation refining abstract types in Flow |
| 373 | return children; |
| 374 | } |
| 375 | const result: Array<React$Node> = []; |
| 376 | let count = 0; |
| 377 | mapIntoArray(children, result, '', '', function (child) { |
| 378 | return func.call(context, child, count++); |
| 379 | }); |
| 380 | return result; |
| 381 | } |
| 382 | |
| 383 | /** |
| 384 | * Count the number of children that are typically specified as |
| 385 | * `props.children`. |
| 386 | * |
| 387 | * See https://reactjs.org/docs/react-api.html#reactchildrencount |
| 388 | * |
| 389 | * @param {?*} children Children tree container. |
| 390 | * @return {number} The number of children. |
| 391 | */ |
| 392 | function countChildren(children: ?ReactNodeList): number { |
| 393 | let n = 0; |
| 394 | mapChildren(children, () => { |
| 395 | n++; |
| 396 | // Don't return anything |
| 397 | }); |
| 398 | return n; |
| 399 | } |
| 400 | |
| 401 | type ForEachFunc = (child: ?React$Node) => void; |
| 402 | |
| 403 | /** |
| 404 | * Iterates through children that are typically specified as `props.children`. |
| 405 | * |
| 406 | * See https://reactjs.org/docs/react-api.html#reactchildrenforeach |
| 407 | * |
| 408 | * The provided forEachFunc(child, index) will be called for each |
| 409 | * leaf child. |
| 410 | * |
| 411 | * @param {?*} children Children tree container. |
| 412 | * @param {function(*, int)} forEachFunc |
| 413 | * @param {*} forEachContext Context for forEachContext. |
| 414 | */ |
| 415 | function forEachChildren( |
| 416 | children: ?ReactNodeList, |
| 417 | forEachFunc: ForEachFunc, |
| 418 | forEachContext: mixed, |
| 419 | ): void { |
| 420 | mapChildren( |
| 421 | children, |
| 422 | // $FlowFixMe[missing-this-annot] |
| 423 | function () { |
| 424 | forEachFunc.apply(this, arguments); |
| 425 | // Don't return anything. |
| 426 | }, |
| 427 | forEachContext, |
| 428 | ); |
| 429 | } |
| 430 | |
| 431 | /** |
| 432 | * Flatten a children object (typically specified as `props.children`) and |
| 433 | * return an array with appropriately re-keyed children. |
| 434 | * |
| 435 | * See https://reactjs.org/docs/react-api.html#reactchildrentoarray |
| 436 | */ |
| 437 | function toArray(children: ?ReactNodeList): Array<React$Node> { |
| 438 | return mapChildren(children, child => child) || []; |
| 439 | } |
| 440 | |
| 441 | /** |
| 442 | * Returns the first child in a collection of children and verifies that there |
| 443 | * is only one child in the collection. |
| 444 | * |
| 445 | * See https://reactjs.org/docs/react-api.html#reactchildrenonly |
| 446 | * |
| 447 | * The current implementation of this function assumes that a single child gets |
| 448 | * passed without a wrapper, but the purpose of this helper function is to |
| 449 | * abstract away the particular structure of children. |
| 450 | * |
| 451 | * @param {?object} children Child collection structure. |
| 452 | * @return {ReactElement} The first and only `ReactElement` contained in the |
| 453 | * structure. |
| 454 | */ |
| 455 | function onlyChild<T>(children: T): T { |
| 456 | if (!isValidElement(children)) { |
| 457 | throw new Error( |
| 458 | 'React.Children.only expected to receive a single React element child.', |
| 459 | ); |
| 460 | } |
| 461 | |
| 462 | return children; |
| 463 | } |
| 464 | |
| 465 | export { |
| 466 | forEachChildren as forEach, |
| 467 | mapChildren as map, |
| 468 | countChildren as count, |
| 469 | onlyChild as only, |
| 470 | toArray, |
| 471 | }; |