diff --git a/.flowconfig b/.flowconfig index d613bab513..00740cac42 100644 --- a/.flowconfig +++ b/.flowconfig @@ -27,6 +27,7 @@ shells/dev/build/* [options] server.max_workers=4 esproposal.class_instance_fields=enable +esproposal.optional_chaining=enable suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe suppress_comment=\\(.\\|\n\\)*\\$FlowIssue suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore diff --git a/src/backend/renderer.js b/src/backend/renderer.js index b5a8557fbf..b0a93dd461 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -249,7 +249,7 @@ export function attach( if (__DEBUG__) { const displayName = getDisplayNameForFiber(fiber) || 'null'; const parentDisplayName = - (parentFiber !== null && getDisplayNameForFiber(parentFiber)) || 'null'; + (parentFiber != null && getDisplayNameForFiber(parentFiber)) || 'null'; // NOTE: calling getFiberID or getPrimaryFiber is unsafe here // because it will put them in the map. For now, we'll omit them. // TODO: better debugging story for this. @@ -634,7 +634,6 @@ export function attach( } } - // eslint-disable-next-line no-unused-vars function haveProfilerTimesChanged( prevFiber: Fiber, nextFiber: Fiber @@ -776,7 +775,7 @@ export function attach( const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration'); if (isProfilingSupported) { idToRootMap.set(id, currentRootID); - idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration); + idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration || 0); } const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner'); @@ -795,7 +794,9 @@ export function attach( const ownerID = _debugOwner != null ? getFiberID(getPrimaryFiber(_debugOwner)) : 0; - const parentID = getFiberID(getPrimaryFiber(parentFiber)); + const parentID = parentFiber + ? getFiberID(getPrimaryFiber(parentFiber)) + : 0; let displayNameStringID = getStringID(displayName); let keyStringID = getStringID(key); @@ -811,14 +812,14 @@ export function attach( if (isProfiling) { // Tree base duration updates are included in the operations typed array. // So we have to convert them from milliseconds to microseconds so we can send them as ints. - const treeBaseDuration = Math.floor(fiber.treeBaseDuration * 1000); + const treeBaseDuration = Math.floor((fiber.treeBaseDuration || 0) * 1000); pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION); pushOperation(id); pushOperation(treeBaseDuration); const { actualDuration } = fiber; - if (actualDuration > 0) { + if (actualDuration != null && actualDuration > 0) { // If profiling is active, store durations for elements that were rendered during the commit. const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData); metadata.actualDurations.push(id, actualDuration); @@ -912,8 +913,12 @@ export function attach( // get the fallback child from the inner fragment and mount // it as if it was our own child. Updates handle this too. const primaryChildFragment = fiber.child; - const fallbackChildFragment = primaryChildFragment.sibling; - const fallbackChild = fallbackChildFragment.child; + const fallbackChildFragment = primaryChildFragment + ? primaryChildFragment.sibling + : null; + const fallbackChild = fallbackChildFragment + ? fallbackChildFragment.child + : null; if (fallbackChild !== null) { mountFiberRecursively( fallbackChild, @@ -956,9 +961,11 @@ export function attach( if (isTimedOutSuspense) { // If it's showing fallback tree, let's traverse it instead. const primaryChildFragment = fiber.child; - const fallbackChildFragment = primaryChildFragment.sibling; + const fallbackChildFragment = primaryChildFragment + ? primaryChildFragment.sibling + : null; // Skip over to the real Fiber child. - child = fallbackChildFragment.child; + child = fallbackChildFragment ? fallbackChildFragment.child : null; } while (child !== null) { @@ -976,20 +983,27 @@ export function attach( const id = getFiberID(getPrimaryFiber(fiber)); const { actualDuration, treeBaseDuration } = fiber; - idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration); + idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration || 0); if (isProfiling) { - if (treeBaseDuration !== fiber.alternate.treeBaseDuration) { + const { alternate } = fiber; + + if ( + treeBaseDuration !== + (alternate ? alternate.treeBaseDuration : undefined) + ) { // Tree base duration updates are included in the operations typed array. // So we have to convert them from milliseconds to microseconds so we can send them as ints. - const treeBaseDuration = Math.floor(fiber.treeBaseDuration * 1000); + const treeBaseDuration = Math.floor( + (fiber.treeBaseDuration || 0) * 1000 + ); pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION); pushOperation(getFiberID(getPrimaryFiber(fiber))); pushOperation(treeBaseDuration); } - if (haveProfilerTimesChanged(fiber.alternate, fiber)) { - if (actualDuration > 0) { + if (alternate ? haveProfilerTimesChanged(alternate, fiber) : true) { + if (actualDuration != null && actualDuration > 0) { // If profiling is active, store durations for elements that were rendered during the commit. const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData); metadata.actualDurations.push(id, actualDuration); @@ -1071,11 +1085,19 @@ export function attach( if (prevDidTimeout && nextDidTimeOut) { // Fallback -> Fallback: // 1. Reconcile fallback set. - const nextFallbackChildSet = nextFiber.child.sibling; + const nextFiberChild = nextFiber.child; + const nextFallbackChildSet = nextFiberChild + ? nextFiberChild.sibling + : null; // Note: We can't use nextFiber.child.sibling.alternate // because the set is special and alternate may not exist. - const prevFallbackChildSet = prevFiber.child.sibling; + const prevFiberChild = prevFiber.child; + const prevFallbackChildSet = prevFiberChild + ? prevFiberChild.sibling + : null; if ( + nextFallbackChildSet != null && + prevFallbackChildSet != null && updateFiberRecursively( nextFallbackChildSet, prevFallbackChildSet, @@ -1101,9 +1123,14 @@ export function attach( // We need to manually walk the previous tree and record unmounts. unmountFiberChildrenRecursively(prevFiber); // 2. Mount fallback set - const nextFallbackChildSet = nextFiber.child.sibling; - mountFiberRecursively(nextFallbackChildSet, nextFiber, true); - shouldResetChildren = true; + const nextFiberChild = nextFiber.child; + const nextFallbackChildSet = nextFiberChild + ? nextFiberChild.sibling + : null; + if (nextFallbackChildSet != null) { + mountFiberRecursively(nextFallbackChildSet, nextFiber, true); + shouldResetChildren = true; + } } else { // Common case: Primary -> Primary. // This is the same codepath as for non-Suspense fibers. @@ -1172,9 +1199,12 @@ export function attach( let nextChildSet = nextFiber.child; if (nextDidTimeOut) { // Special case: timed-out Suspense renders the fallback set. - nextChildSet = nextFiber.child.sibling; + const nextFiberChild = nextFiber.child; + nextChildSet = nextFiberChild ? nextFiberChild.sibling : null; + } + if (nextChildSet != null) { + recordResetChildren(nextFiber, nextChildSet); } - recordResetChildren(nextFiber, nextChildSet); // We've handled the child order change for this Fiber. // Since it's included, there's no need to invalidate parent child order. return false; @@ -1357,7 +1387,7 @@ export function attach( function findNativeByFiberID(id: number) { try { - let fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); + let fiber = findCurrentFiberUsingSlowPathById(id); if (fiber === null) { return null; } @@ -1367,9 +1397,10 @@ export function attach( if (isTimedOutSuspense) { // A timed-out Suspense's findDOMNode is useless. // Try our best to find the fallback directly. - const maybeFallbackFiber = - (fiber.child && fiber.child.sibling) || fiber; - fiber = maybeFallbackFiber; + const maybeFallbackFiber = fiber.child && fiber.child.sibling; + if (maybeFallbackFiber != null) { + fiber = maybeFallbackFiber; + } } const hostFibers = findAllCurrentHostFibers(fiber); return hostFibers.map(hostFiber => hostFiber.stateNode).filter(Boolean); @@ -1647,6 +1678,11 @@ export function attach( // Find the currently mounted version of this fiber (so we don't show the wrong props and state). fiber = findCurrentFiberUsingSlowPath(fiber); + if (fiber == null) { + console.warn(`Could not find Fiber with id "${id}"`); + return null; + } + const { _debugOwner, _debugSource, @@ -1655,7 +1691,7 @@ export function attach( memoizedState, tag, type, - } = ((fiber: any): Fiber); + } = fiber; const usesHooks = (tag === FunctionComponent || @@ -1729,7 +1765,7 @@ export function attach( displayName: getDisplayNameForFiber(owner) || 'Unknown', id: getFiberID(getPrimaryFiber(owner)), }); - owner = owner._debugOwner; + owner = owner._debugOwner || null; } } @@ -1826,13 +1862,21 @@ export function attach( } } + function findCurrentFiberUsingSlowPathById(id: number): Fiber | null { + const fiber = idToFiberMap.get(id); + if (fiber) { + return findCurrentFiberUsingSlowPath(fiber); + } + return null; + } + function setInHook( id: number, index: number, path: Array, value: any ) { - const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); + const fiber = findCurrentFiberUsingSlowPathById(id); if (fiber !== null) { if (typeof overrideHookState === 'function') { overrideHookState(fiber, index, path, value); @@ -1841,7 +1885,7 @@ export function attach( } function setInProps(id: number, path: Array, value: any) { - const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); + const fiber = findCurrentFiberUsingSlowPathById(id); if (fiber !== null) { const instance = fiber.stateNode; if (instance === null) { @@ -1856,7 +1900,7 @@ export function attach( } function setInState(id: number, path: Array, value: any) { - const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); + const fiber = findCurrentFiberUsingSlowPathById(id); if (fiber !== null) { const instance = fiber.stateNode; setInObject(instance.state, path, value); @@ -1870,7 +1914,7 @@ export function attach( // We need to remove the first part of the path (the "value") before continuing. path = path.slice(1); - const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); + const fiber = findCurrentFiberUsingSlowPathById(id); if (fiber !== null) { const instance = fiber.stateNode; if (path.length === 0) { @@ -2043,17 +2087,18 @@ export function attach( } const initialTreeBaseDurations = []; - ((initialTreeBaseDurationsMap: any): Map).forEach( - (treeBaseDuration, id) => { + if (initialTreeBaseDurationsMap != null) { + initialTreeBaseDurationsMap.forEach((treeBaseDuration, id) => { if ( - ((initialIDToRootMap: any): Map).get(id) === rootID + initialIDToRootMap != null && + initialIDToRootMap.get(id) === rootID ) { // We don't need to convert milliseconds to microseconds in this case, // because the profiling summary is JSON serialized. initialTreeBaseDurations.push(id, treeBaseDuration); } - } - ); + }); + } return { commitDurations, diff --git a/src/backend/types.js b/src/backend/types.js index 4695d7706b..7cd529528b 100644 --- a/src/backend/types.js +++ b/src/backend/types.js @@ -7,8 +7,68 @@ type BundleType = | 0 // PROD | 1; // DEV -// TODO: Better type for Fiber -export type Fiber = Object; +export type WorkTag = number; +export type SideEffectTag = number; +export type ExpirationTime = number; +export type RefObject = {| + current: any, +|}; +export type Source = { + fileName: string, + lineNumber: number, +}; +export type HookType = + | 'useState' + | 'useReducer' + | 'useContext' + | 'useRef' + | 'useEffect' + | 'useLayoutEffect' + | 'useCallback' + | 'useMemo' + | 'useImperativeHandle' + | 'useDebugValue'; + +// The Fiber type is copied from React and should be kept in sync: +// https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiber.js +// The properties we don't use in DevTools are omitted. +export type Fiber = {| + tag: WorkTag, + + key: null | string, + + elementType: any, + + type: any, + + stateNode: any, + + return: Fiber | null, + + child: Fiber | null, + sibling: Fiber | null, + index: number, + + ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject, + + pendingProps: any, // This type will be more specific once we overload the tag. + memoizedProps: any, // The props used to create the output. + + memoizedState: any, + + effectTag: SideEffectTag, + + alternate: Fiber | null, + + actualDuration?: number, + + actualStartTime?: number, + + treeBaseDuration?: number, + + _debugSource?: Source | null, + _debugOwner?: Fiber | null, +|}; // TODO: If it's useful for the frontend to know which types of data an Element has // (e.g. props, state, context, hooks) then we could add a bitmask field for this