mirror of
https://github.com/facebook/react.git
synced 2026-02-26 18:58:05 +00:00
* add transition name to startTransition
Add a transitionName to start transition, store the transition start time and name in the batch config, and pass it to the root on render
* Transition Tracing Types and Consts
* Root begin work
The root operates as a tracing marker that has all transitions on it. This PR only tested the root with one transition so far
- Store transitions in memoizedState. Do this in updateHostRoot AND attemptEarlyBailoutIfNoScheduledUpdate. We need to do this in the latter part because even if the root itself doesn't have an update, it could still have new transitions in its transitionLanes map that we need to process.
* Transition Tracing commit phase
- adds a module scoped pending transition callbacks object that contains all transition callbacks that have not yet been processed. This contains all callbacks before the next paint occurs.
- Add code in the mutation phase to:
* For the root, if there are transitions that were initialized during this commit in the root transition lanes map, add a transition start call to the pending transition callbacks object. Then, remove the transitions from the root transition lanes map.
* For roots, in the commit phase, add a transition complete call
We add this code in the mutation phase because we can't add it to the passive phase because then the paint might have occurred before we even know which callbacks to call
* Process Callbacks after paint
At the end of the commit phase, call scheduleTransitionCallbacks to schedule all pending transition callbacks to be called after paint. Then clear the callbacks
788 lines
22 KiB
JavaScript
788 lines
22 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {
|
|
MutableSource,
|
|
MutableSourceGetSnapshotFn,
|
|
MutableSourceSubscribeFn,
|
|
ReactContext,
|
|
ReactProviderType,
|
|
StartTransitionOptions,
|
|
} from 'shared/ReactTypes';
|
|
import type {
|
|
Fiber,
|
|
Dispatcher as DispatcherType,
|
|
} from 'react-reconciler/src/ReactInternalTypes';
|
|
|
|
import ErrorStackParser from 'error-stack-parser';
|
|
import assign from 'shared/assign';
|
|
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
|
import {
|
|
FunctionComponent,
|
|
SimpleMemoComponent,
|
|
ContextProvider,
|
|
ForwardRef,
|
|
} from 'react-reconciler/src/ReactWorkTags';
|
|
|
|
type CurrentDispatcherRef = typeof ReactSharedInternals.ReactCurrentDispatcher;
|
|
|
|
// Used to track hooks called during a render
|
|
|
|
type HookLogEntry = {
|
|
primitive: string,
|
|
stackError: Error,
|
|
value: mixed,
|
|
...
|
|
};
|
|
|
|
let hookLog: Array<HookLogEntry> = [];
|
|
|
|
// Primitives
|
|
|
|
type BasicStateAction<S> = (S => S) | S;
|
|
|
|
type Dispatch<A> = A => void;
|
|
|
|
let primitiveStackCache: null | Map<string, Array<any>> = null;
|
|
|
|
type Hook = {
|
|
memoizedState: any,
|
|
next: Hook | null,
|
|
};
|
|
|
|
function getPrimitiveStackCache(): Map<string, Array<any>> {
|
|
// This initializes a cache of all primitive hooks so that the top
|
|
// most stack frames added by calling the primitive hook can be removed.
|
|
if (primitiveStackCache === null) {
|
|
const cache = new Map();
|
|
let readHookLog;
|
|
try {
|
|
// Use all hooks here to add them to the hook log.
|
|
Dispatcher.useContext(({_currentValue: null}: any));
|
|
Dispatcher.useState(null);
|
|
Dispatcher.useReducer((s, a) => s, null);
|
|
Dispatcher.useRef(null);
|
|
if (typeof Dispatcher.useCacheRefresh === 'function') {
|
|
// This type check is for Flow only.
|
|
Dispatcher.useCacheRefresh();
|
|
}
|
|
Dispatcher.useLayoutEffect(() => {});
|
|
Dispatcher.useInsertionEffect(() => {});
|
|
Dispatcher.useEffect(() => {});
|
|
Dispatcher.useImperativeHandle(undefined, () => null);
|
|
Dispatcher.useDebugValue(null);
|
|
Dispatcher.useCallback(() => {});
|
|
Dispatcher.useMemo(() => null);
|
|
} finally {
|
|
readHookLog = hookLog;
|
|
hookLog = [];
|
|
}
|
|
for (let i = 0; i < readHookLog.length; i++) {
|
|
const hook = readHookLog[i];
|
|
cache.set(hook.primitive, ErrorStackParser.parse(hook.stackError));
|
|
}
|
|
primitiveStackCache = cache;
|
|
}
|
|
return primitiveStackCache;
|
|
}
|
|
|
|
let currentHook: null | Hook = null;
|
|
function nextHook(): null | Hook {
|
|
const hook = currentHook;
|
|
if (hook !== null) {
|
|
currentHook = hook.next;
|
|
}
|
|
return hook;
|
|
}
|
|
|
|
function getCacheForType<T>(resourceType: () => T): T {
|
|
throw new Error('Not implemented.');
|
|
}
|
|
|
|
function readContext<T>(context: ReactContext<T>): T {
|
|
// For now we don't expose readContext usage in the hooks debugging info.
|
|
return context._currentValue;
|
|
}
|
|
|
|
function useContext<T>(context: ReactContext<T>): T {
|
|
hookLog.push({
|
|
primitive: 'Context',
|
|
stackError: new Error(),
|
|
value: context._currentValue,
|
|
});
|
|
return context._currentValue;
|
|
}
|
|
|
|
function useState<S>(
|
|
initialState: (() => S) | S,
|
|
): [S, Dispatch<BasicStateAction<S>>] {
|
|
const hook = nextHook();
|
|
const state: S =
|
|
hook !== null
|
|
? hook.memoizedState
|
|
: typeof initialState === 'function'
|
|
? // $FlowFixMe: Flow doesn't like mixed types
|
|
initialState()
|
|
: initialState;
|
|
hookLog.push({primitive: 'State', stackError: new Error(), value: state});
|
|
return [state, (action: BasicStateAction<S>) => {}];
|
|
}
|
|
|
|
function useReducer<S, I, A>(
|
|
reducer: (S, A) => S,
|
|
initialArg: I,
|
|
init?: I => S,
|
|
): [S, Dispatch<A>] {
|
|
const hook = nextHook();
|
|
let state;
|
|
if (hook !== null) {
|
|
state = hook.memoizedState;
|
|
} else {
|
|
state = init !== undefined ? init(initialArg) : ((initialArg: any): S);
|
|
}
|
|
hookLog.push({
|
|
primitive: 'Reducer',
|
|
stackError: new Error(),
|
|
value: state,
|
|
});
|
|
return [state, (action: A) => {}];
|
|
}
|
|
|
|
function useRef<T>(initialValue: T): {|current: T|} {
|
|
const hook = nextHook();
|
|
const ref = hook !== null ? hook.memoizedState : {current: initialValue};
|
|
hookLog.push({
|
|
primitive: 'Ref',
|
|
stackError: new Error(),
|
|
value: ref.current,
|
|
});
|
|
return ref;
|
|
}
|
|
|
|
function useCacheRefresh(): () => void {
|
|
const hook = nextHook();
|
|
hookLog.push({
|
|
primitive: 'CacheRefresh',
|
|
stackError: new Error(),
|
|
value: hook !== null ? hook.memoizedState : function refresh() {},
|
|
});
|
|
return () => {};
|
|
}
|
|
|
|
function useLayoutEffect(
|
|
create: () => (() => void) | void,
|
|
inputs: Array<mixed> | void | null,
|
|
): void {
|
|
nextHook();
|
|
hookLog.push({
|
|
primitive: 'LayoutEffect',
|
|
stackError: new Error(),
|
|
value: create,
|
|
});
|
|
}
|
|
|
|
function useInsertionEffect(
|
|
create: () => mixed,
|
|
inputs: Array<mixed> | void | null,
|
|
): void {
|
|
nextHook();
|
|
hookLog.push({
|
|
primitive: 'InsertionEffect',
|
|
stackError: new Error(),
|
|
value: create,
|
|
});
|
|
}
|
|
|
|
function useEffect(
|
|
create: () => (() => void) | void,
|
|
inputs: Array<mixed> | void | null,
|
|
): void {
|
|
nextHook();
|
|
hookLog.push({primitive: 'Effect', stackError: new Error(), value: create});
|
|
}
|
|
|
|
function useImperativeHandle<T>(
|
|
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
|
|
create: () => T,
|
|
inputs: Array<mixed> | void | null,
|
|
): void {
|
|
nextHook();
|
|
// We don't actually store the instance anywhere if there is no ref callback
|
|
// and if there is a ref callback it might not store it but if it does we
|
|
// have no way of knowing where. So let's only enable introspection of the
|
|
// ref itself if it is using the object form.
|
|
let instance = undefined;
|
|
if (ref !== null && typeof ref === 'object') {
|
|
instance = ref.current;
|
|
}
|
|
hookLog.push({
|
|
primitive: 'ImperativeHandle',
|
|
stackError: new Error(),
|
|
value: instance,
|
|
});
|
|
}
|
|
|
|
function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
|
|
hookLog.push({
|
|
primitive: 'DebugValue',
|
|
stackError: new Error(),
|
|
value: typeof formatterFn === 'function' ? formatterFn(value) : value,
|
|
});
|
|
}
|
|
|
|
function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
|
|
const hook = nextHook();
|
|
hookLog.push({
|
|
primitive: 'Callback',
|
|
stackError: new Error(),
|
|
value: hook !== null ? hook.memoizedState[0] : callback,
|
|
});
|
|
return callback;
|
|
}
|
|
|
|
function useMemo<T>(
|
|
nextCreate: () => T,
|
|
inputs: Array<mixed> | void | null,
|
|
): T {
|
|
const hook = nextHook();
|
|
const value = hook !== null ? hook.memoizedState[0] : nextCreate();
|
|
hookLog.push({primitive: 'Memo', stackError: new Error(), value});
|
|
return value;
|
|
}
|
|
|
|
function useMutableSource<Source, Snapshot>(
|
|
source: MutableSource<Source>,
|
|
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
|
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
|
): Snapshot {
|
|
// useMutableSource() composes multiple hooks internally.
|
|
// Advance the current hook index the same number of times
|
|
// so that subsequent hooks have the right memoized state.
|
|
nextHook(); // MutableSource
|
|
nextHook(); // State
|
|
nextHook(); // Effect
|
|
nextHook(); // Effect
|
|
const value = getSnapshot(source._source);
|
|
hookLog.push({primitive: 'MutableSource', stackError: new Error(), value});
|
|
return value;
|
|
}
|
|
|
|
function useSyncExternalStore<T>(
|
|
subscribe: (() => void) => () => void,
|
|
getSnapshot: () => T,
|
|
getServerSnapshot?: () => T,
|
|
): T {
|
|
// useSyncExternalStore() composes multiple hooks internally.
|
|
// Advance the current hook index the same number of times
|
|
// so that subsequent hooks have the right memoized state.
|
|
nextHook(); // SyncExternalStore
|
|
nextHook(); // Effect
|
|
const value = getSnapshot();
|
|
hookLog.push({
|
|
primitive: 'SyncExternalStore',
|
|
stackError: new Error(),
|
|
value,
|
|
});
|
|
return value;
|
|
}
|
|
|
|
function useTransition(): [
|
|
boolean,
|
|
(callback: () => void, options?: StartTransitionOptions) => void,
|
|
] {
|
|
// useTransition() composes multiple hooks internally.
|
|
// Advance the current hook index the same number of times
|
|
// so that subsequent hooks have the right memoized state.
|
|
nextHook(); // State
|
|
nextHook(); // Callback
|
|
hookLog.push({
|
|
primitive: 'Transition',
|
|
stackError: new Error(),
|
|
value: undefined,
|
|
});
|
|
return [false, callback => {}];
|
|
}
|
|
|
|
function useDeferredValue<T>(value: T): T {
|
|
// useDeferredValue() composes multiple hooks internally.
|
|
// Advance the current hook index the same number of times
|
|
// so that subsequent hooks have the right memoized state.
|
|
nextHook(); // State
|
|
nextHook(); // Effect
|
|
hookLog.push({
|
|
primitive: 'DeferredValue',
|
|
stackError: new Error(),
|
|
value,
|
|
});
|
|
return value;
|
|
}
|
|
|
|
function useId(): string {
|
|
const hook = nextHook();
|
|
const id = hook !== null ? hook.memoizedState : '';
|
|
hookLog.push({
|
|
primitive: 'Id',
|
|
stackError: new Error(),
|
|
value: id,
|
|
});
|
|
return id;
|
|
}
|
|
|
|
const Dispatcher: DispatcherType = {
|
|
getCacheForType,
|
|
readContext,
|
|
useCacheRefresh,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useImperativeHandle,
|
|
useDebugValue,
|
|
useLayoutEffect,
|
|
useInsertionEffect,
|
|
useMemo,
|
|
useReducer,
|
|
useRef,
|
|
useState,
|
|
useTransition,
|
|
useMutableSource,
|
|
useSyncExternalStore,
|
|
useDeferredValue,
|
|
useId,
|
|
};
|
|
|
|
// Inspect
|
|
|
|
export type HookSource = {
|
|
lineNumber: number | null,
|
|
columnNumber: number | null,
|
|
fileName: string | null,
|
|
functionName: string | null,
|
|
};
|
|
|
|
export type HooksNode = {
|
|
id: number | null,
|
|
isStateEditable: boolean,
|
|
name: string,
|
|
value: mixed,
|
|
subHooks: Array<HooksNode>,
|
|
hookSource?: HookSource,
|
|
...
|
|
};
|
|
export type HooksTree = Array<HooksNode>;
|
|
|
|
// Don't assume
|
|
//
|
|
// We can't assume that stack frames are nth steps away from anything.
|
|
// E.g. we can't assume that the root call shares all frames with the stack
|
|
// of a hook call. A simple way to demonstrate this is wrapping `new Error()`
|
|
// in a wrapper constructor like a polyfill. That'll add an extra frame.
|
|
// Similar things can happen with the call to the dispatcher. The top frame
|
|
// may not be the primitive. Likewise the primitive can have fewer stack frames
|
|
// such as when a call to useState got inlined to use dispatcher.useState.
|
|
//
|
|
// We also can't assume that the last frame of the root call is the same
|
|
// frame as the last frame of the hook call because long stack traces can be
|
|
// truncated to a stack trace limit.
|
|
|
|
let mostLikelyAncestorIndex = 0;
|
|
|
|
function findSharedIndex(hookStack, rootStack, rootIndex) {
|
|
const source = rootStack[rootIndex].source;
|
|
hookSearch: for (let i = 0; i < hookStack.length; i++) {
|
|
if (hookStack[i].source === source) {
|
|
// This looks like a match. Validate that the rest of both stack match up.
|
|
for (
|
|
let a = rootIndex + 1, b = i + 1;
|
|
a < rootStack.length && b < hookStack.length;
|
|
a++, b++
|
|
) {
|
|
if (hookStack[b].source !== rootStack[a].source) {
|
|
// If not, give up and try a different match.
|
|
continue hookSearch;
|
|
}
|
|
}
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function findCommonAncestorIndex(rootStack, hookStack) {
|
|
let rootIndex = findSharedIndex(
|
|
hookStack,
|
|
rootStack,
|
|
mostLikelyAncestorIndex,
|
|
);
|
|
if (rootIndex !== -1) {
|
|
return rootIndex;
|
|
}
|
|
// If the most likely one wasn't a hit, try any other frame to see if it is shared.
|
|
// If that takes more than 5 frames, something probably went wrong.
|
|
for (let i = 0; i < rootStack.length && i < 5; i++) {
|
|
rootIndex = findSharedIndex(hookStack, rootStack, i);
|
|
if (rootIndex !== -1) {
|
|
mostLikelyAncestorIndex = i;
|
|
return rootIndex;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function isReactWrapper(functionName, primitiveName) {
|
|
if (!functionName) {
|
|
return false;
|
|
}
|
|
const expectedPrimitiveName = 'use' + primitiveName;
|
|
if (functionName.length < expectedPrimitiveName.length) {
|
|
return false;
|
|
}
|
|
return (
|
|
functionName.lastIndexOf(expectedPrimitiveName) ===
|
|
functionName.length - expectedPrimitiveName.length
|
|
);
|
|
}
|
|
|
|
function findPrimitiveIndex(hookStack, hook) {
|
|
const stackCache = getPrimitiveStackCache();
|
|
const primitiveStack = stackCache.get(hook.primitive);
|
|
if (primitiveStack === undefined) {
|
|
return -1;
|
|
}
|
|
for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {
|
|
if (primitiveStack[i].source !== hookStack[i].source) {
|
|
// If the next two frames are functions called `useX` then we assume that they're part of the
|
|
// wrappers that the React packager or other packages adds around the dispatcher.
|
|
if (
|
|
i < hookStack.length - 1 &&
|
|
isReactWrapper(hookStack[i].functionName, hook.primitive)
|
|
) {
|
|
i++;
|
|
}
|
|
if (
|
|
i < hookStack.length - 1 &&
|
|
isReactWrapper(hookStack[i].functionName, hook.primitive)
|
|
) {
|
|
i++;
|
|
}
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function parseTrimmedStack(rootStack, hook) {
|
|
// Get the stack trace between the primitive hook function and
|
|
// the root function call. I.e. the stack frames of custom hooks.
|
|
const hookStack = ErrorStackParser.parse(hook.stackError);
|
|
const rootIndex = findCommonAncestorIndex(rootStack, hookStack);
|
|
const primitiveIndex = findPrimitiveIndex(hookStack, hook);
|
|
if (
|
|
rootIndex === -1 ||
|
|
primitiveIndex === -1 ||
|
|
rootIndex - primitiveIndex < 2
|
|
) {
|
|
// Something went wrong. Give up.
|
|
return null;
|
|
}
|
|
return hookStack.slice(primitiveIndex, rootIndex - 1);
|
|
}
|
|
|
|
function parseCustomHookName(functionName: void | string): string {
|
|
if (!functionName) {
|
|
return '';
|
|
}
|
|
let startIndex = functionName.lastIndexOf('.');
|
|
if (startIndex === -1) {
|
|
startIndex = 0;
|
|
}
|
|
if (functionName.substr(startIndex, 3) === 'use') {
|
|
startIndex += 3;
|
|
}
|
|
return functionName.substr(startIndex);
|
|
}
|
|
|
|
function buildTree(
|
|
rootStack,
|
|
readHookLog,
|
|
includeHooksSource: boolean,
|
|
): HooksTree {
|
|
const rootChildren = [];
|
|
let prevStack = null;
|
|
let levelChildren = rootChildren;
|
|
let nativeHookID = 0;
|
|
const stackOfChildren = [];
|
|
for (let i = 0; i < readHookLog.length; i++) {
|
|
const hook = readHookLog[i];
|
|
const stack = parseTrimmedStack(rootStack, hook);
|
|
if (stack !== null) {
|
|
// Note: The indices 0 <= n < length-1 will contain the names.
|
|
// The indices 1 <= n < length will contain the source locations.
|
|
// That's why we get the name from n - 1 and don't check the source
|
|
// of index 0.
|
|
let commonSteps = 0;
|
|
if (prevStack !== null) {
|
|
// Compare the current level's stack to the new stack.
|
|
while (commonSteps < stack.length && commonSteps < prevStack.length) {
|
|
const stackSource = stack[stack.length - commonSteps - 1].source;
|
|
const prevSource =
|
|
prevStack[prevStack.length - commonSteps - 1].source;
|
|
if (stackSource !== prevSource) {
|
|
break;
|
|
}
|
|
commonSteps++;
|
|
}
|
|
// Pop back the stack as many steps as were not common.
|
|
for (let j = prevStack.length - 1; j > commonSteps; j--) {
|
|
levelChildren = stackOfChildren.pop();
|
|
}
|
|
}
|
|
// The remaining part of the new stack are custom hooks. Push them
|
|
// to the tree.
|
|
for (let j = stack.length - commonSteps - 1; j >= 1; j--) {
|
|
const children = [];
|
|
const stackFrame = stack[j];
|
|
const levelChild: HooksNode = {
|
|
id: null,
|
|
isStateEditable: false,
|
|
name: parseCustomHookName(stack[j - 1].functionName),
|
|
value: undefined,
|
|
subHooks: children,
|
|
};
|
|
|
|
if (includeHooksSource) {
|
|
levelChild.hookSource = {
|
|
lineNumber: stackFrame.lineNumber,
|
|
columnNumber: stackFrame.columnNumber,
|
|
functionName: stackFrame.functionName,
|
|
fileName: stackFrame.fileName,
|
|
};
|
|
}
|
|
|
|
levelChildren.push(levelChild);
|
|
stackOfChildren.push(levelChildren);
|
|
levelChildren = children;
|
|
}
|
|
prevStack = stack;
|
|
}
|
|
const {primitive} = hook;
|
|
|
|
// For now, the "id" of stateful hooks is just the stateful hook index.
|
|
// Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
|
|
const id =
|
|
primitive === 'Context' || primitive === 'DebugValue'
|
|
? null
|
|
: nativeHookID++;
|
|
|
|
// For the time being, only State and Reducer hooks support runtime overrides.
|
|
const isStateEditable = primitive === 'Reducer' || primitive === 'State';
|
|
const levelChild: HooksNode = {
|
|
id,
|
|
isStateEditable,
|
|
name: primitive,
|
|
value: hook.value,
|
|
subHooks: [],
|
|
};
|
|
|
|
if (includeHooksSource) {
|
|
const hookSource: HookSource = {
|
|
lineNumber: null,
|
|
functionName: null,
|
|
fileName: null,
|
|
columnNumber: null,
|
|
};
|
|
if (stack && stack.length >= 1) {
|
|
const stackFrame = stack[0];
|
|
hookSource.lineNumber = stackFrame.lineNumber;
|
|
hookSource.functionName = stackFrame.functionName;
|
|
hookSource.fileName = stackFrame.fileName;
|
|
hookSource.columnNumber = stackFrame.columnNumber;
|
|
}
|
|
|
|
levelChild.hookSource = hookSource;
|
|
}
|
|
|
|
levelChildren.push(levelChild);
|
|
}
|
|
|
|
// Associate custom hook values (useDebugValue() hook entries) with the correct hooks.
|
|
processDebugValues(rootChildren, null);
|
|
|
|
return rootChildren;
|
|
}
|
|
|
|
// Custom hooks support user-configurable labels (via the special useDebugValue() hook).
|
|
// That hook adds user-provided values to the hooks tree,
|
|
// but these values aren't intended to appear alongside of the other hooks.
|
|
// Instead they should be attributed to their parent custom hook.
|
|
// This method walks the tree and assigns debug values to their custom hook owners.
|
|
function processDebugValues(
|
|
hooksTree: HooksTree,
|
|
parentHooksNode: HooksNode | null,
|
|
): void {
|
|
const debugValueHooksNodes: Array<HooksNode> = [];
|
|
|
|
for (let i = 0; i < hooksTree.length; i++) {
|
|
const hooksNode = hooksTree[i];
|
|
if (hooksNode.name === 'DebugValue' && hooksNode.subHooks.length === 0) {
|
|
hooksTree.splice(i, 1);
|
|
i--;
|
|
debugValueHooksNodes.push(hooksNode);
|
|
} else {
|
|
processDebugValues(hooksNode.subHooks, hooksNode);
|
|
}
|
|
}
|
|
|
|
// Bubble debug value labels to their custom hook owner.
|
|
// If there is no parent hook, just ignore them for now.
|
|
// (We may warn about this in the future.)
|
|
if (parentHooksNode !== null) {
|
|
if (debugValueHooksNodes.length === 1) {
|
|
parentHooksNode.value = debugValueHooksNodes[0].value;
|
|
} else if (debugValueHooksNodes.length > 1) {
|
|
parentHooksNode.value = debugValueHooksNodes.map(({value}) => value);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function inspectHooks<Props>(
|
|
renderFunction: Props => React$Node,
|
|
props: Props,
|
|
currentDispatcher: ?CurrentDispatcherRef,
|
|
includeHooksSource?: boolean = false,
|
|
): HooksTree {
|
|
// DevTools will pass the current renderer's injected dispatcher.
|
|
// Other apps might compile debug hooks as part of their app though.
|
|
if (currentDispatcher == null) {
|
|
currentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
|
|
}
|
|
|
|
const previousDispatcher = currentDispatcher.current;
|
|
let readHookLog;
|
|
currentDispatcher.current = Dispatcher;
|
|
let ancestorStackError;
|
|
try {
|
|
ancestorStackError = new Error();
|
|
renderFunction(props);
|
|
} finally {
|
|
readHookLog = hookLog;
|
|
hookLog = [];
|
|
currentDispatcher.current = previousDispatcher;
|
|
}
|
|
const rootStack = ErrorStackParser.parse(ancestorStackError);
|
|
return buildTree(rootStack, readHookLog, includeHooksSource);
|
|
}
|
|
|
|
function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
|
|
let current = fiber;
|
|
while (current) {
|
|
if (current.tag === ContextProvider) {
|
|
const providerType: ReactProviderType<any> = current.type;
|
|
const context: ReactContext<any> = providerType._context;
|
|
if (!contextMap.has(context)) {
|
|
// Store the current value that we're going to restore later.
|
|
contextMap.set(context, context._currentValue);
|
|
// Set the inner most provider value on the context.
|
|
context._currentValue = current.memoizedProps.value;
|
|
}
|
|
}
|
|
current = current.return;
|
|
}
|
|
}
|
|
|
|
function restoreContexts(contextMap: Map<ReactContext<any>, any>) {
|
|
contextMap.forEach((value, context) => (context._currentValue = value));
|
|
}
|
|
|
|
function inspectHooksOfForwardRef<Props, Ref>(
|
|
renderFunction: (Props, Ref) => React$Node,
|
|
props: Props,
|
|
ref: Ref,
|
|
currentDispatcher: CurrentDispatcherRef,
|
|
includeHooksSource: boolean,
|
|
): HooksTree {
|
|
const previousDispatcher = currentDispatcher.current;
|
|
let readHookLog;
|
|
currentDispatcher.current = Dispatcher;
|
|
let ancestorStackError;
|
|
try {
|
|
ancestorStackError = new Error();
|
|
renderFunction(props, ref);
|
|
} finally {
|
|
readHookLog = hookLog;
|
|
hookLog = [];
|
|
currentDispatcher.current = previousDispatcher;
|
|
}
|
|
const rootStack = ErrorStackParser.parse(ancestorStackError);
|
|
return buildTree(rootStack, readHookLog, includeHooksSource);
|
|
}
|
|
|
|
function resolveDefaultProps(Component, baseProps) {
|
|
if (Component && Component.defaultProps) {
|
|
// Resolve default props. Taken from ReactElement
|
|
const props = assign({}, baseProps);
|
|
const defaultProps = Component.defaultProps;
|
|
for (const propName in defaultProps) {
|
|
if (props[propName] === undefined) {
|
|
props[propName] = defaultProps[propName];
|
|
}
|
|
}
|
|
return props;
|
|
}
|
|
return baseProps;
|
|
}
|
|
|
|
export function inspectHooksOfFiber(
|
|
fiber: Fiber,
|
|
currentDispatcher: ?CurrentDispatcherRef,
|
|
includeHooksSource?: boolean = false,
|
|
) {
|
|
// DevTools will pass the current renderer's injected dispatcher.
|
|
// Other apps might compile debug hooks as part of their app though.
|
|
if (currentDispatcher == null) {
|
|
currentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
|
|
}
|
|
|
|
if (
|
|
fiber.tag !== FunctionComponent &&
|
|
fiber.tag !== SimpleMemoComponent &&
|
|
fiber.tag !== ForwardRef
|
|
) {
|
|
throw new Error(
|
|
'Unknown Fiber. Needs to be a function component to inspect hooks.',
|
|
);
|
|
}
|
|
// Warm up the cache so that it doesn't consume the currentHook.
|
|
getPrimitiveStackCache();
|
|
const type = fiber.type;
|
|
let props = fiber.memoizedProps;
|
|
if (type !== fiber.elementType) {
|
|
props = resolveDefaultProps(type, props);
|
|
}
|
|
// Set up the current hook so that we can step through and read the
|
|
// current state from them.
|
|
currentHook = (fiber.memoizedState: Hook);
|
|
const contextMap = new Map();
|
|
try {
|
|
setupContexts(contextMap, fiber);
|
|
if (fiber.tag === ForwardRef) {
|
|
return inspectHooksOfForwardRef(
|
|
type.render,
|
|
props,
|
|
fiber.ref,
|
|
currentDispatcher,
|
|
includeHooksSource,
|
|
);
|
|
}
|
|
return inspectHooks(type, props, currentDispatcher, includeHooksSource);
|
|
} finally {
|
|
currentHook = null;
|
|
restoreContexts(contextMap);
|
|
}
|
|
}
|