/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react-noop-renderer/src/createReactNoop.js
1 597 строк
45 KB
Sebastian Markbåge
Log the time until the Animation finishes as "Animating" (#34538)
20 сен 2025, 18:10
Не верифицирован
20 сен 2025, 18:10
b4fe1e6
Код
Авторство
О чём код?
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ /** * This is a renderer of React that doesn't have a render target output. * It is useful to demonstrate the internals of the reconciler in isolation * and for testing semantics of reconciliation separate from the host * environment. */ import type { Fiber, TransitionTracingCallbacks, } from 'react-reconciler/src/ReactInternalTypes'; import type {UpdateQueue} from 'react-reconciler/src/ReactFiberClassUpdateQueue'; import type {ReactNodeList} from 'shared/ReactTypes'; import type {RootTag} from 'react-reconciler/src/ReactRootTags'; import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities'; import type {TransitionTypes} from 'react/src/ReactTransitionType'; import * as Scheduler from 'scheduler/unstable_mock'; import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; import isArray from 'shared/isArray'; import {checkPropStringCoercion} from 'shared/CheckStringCoercion'; import { NoEventPriority, DiscreteEventPriority, DefaultEventPriority, IdleEventPriority, ConcurrentRoot, LegacyRoot, } from 'react-reconciler/constants'; import {disableLegacyMode} from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import ReactVersion from 'shared/ReactVersion'; type Container = { rootID: string, children: Array<Instance | TextInstance>, pendingChildren: Array<Instance | TextInstance>, ... }; type Props = { prop: any, hidden: boolean, children?: mixed, bottom?: null | number, left?: null | number, right?: null | number, top?: null | number, src?: string, ... }; type Instance = { type: string, id: number, parent: number, children: Array<Instance | TextInstance>, text: string | null, prop: any, hidden: boolean, context: HostContext, }; type TextInstance = { text: string, id: number, parent: number, hidden: boolean, context: HostContext, }; type HostContext = Object; type CreateRootOptions = { unstable_transitionCallbacks?: TransitionTracingCallbacks, onUncaughtError?: (error: mixed, errorInfo: {componentStack: string}) => void, onCaughtError?: (error: mixed, errorInfo: {componentStack: string}) => void, onDefaultTransitionIndicator?: () => void | (() => void), ... }; type InstanceMeasurement = null; type SuspenseyCommitSubscription = { pendingCount: number, commit: null | (() => void), }; export opaque type SuspendedState = SuspenseyCommitSubscription; export type TransitionStatus = mixed; export type FormInstance = Instance; export type RunningViewTransition = null; export type ViewTransitionInstance = null | {name: string, ...}; export type GestureTimeline = null; const NO_CONTEXT = {}; const UPPERCASE_CONTEXT = {}; if (__DEV__) { Object.freeze(NO_CONTEXT); } function createReactNoop(reconciler: Function, useMutation: boolean) { let instanceCounter = 0; let hostUpdateCounter = 0; let hostCloneCounter = 0; function appendChildToContainerOrInstance( parentInstance: Container | Instance, child: Instance | TextInstance, ): void { const prevParent = child.parent; if (prevParent !== -1 && prevParent !== parentInstance.id) { throw new Error('Reparenting is not allowed'); } child.parent = parentInstance.id; const index = parentInstance.children.indexOf(child); if (index !== -1) { parentInstance.children.splice(index, 1); } parentInstance.children.push(child); } function appendChildToContainer( parentInstance: Container, child: Instance | TextInstance, ): void { if (typeof parentInstance.rootID !== 'string') { // Some calls to this aren't typesafe. // This helps surface mistakes in tests. throw new Error( 'appendChildToContainer() first argument is not a container.', ); } appendChildToContainerOrInstance(parentInstance, child); } function appendChild( parentInstance: Instance, child: Instance | TextInstance, ): void { if (typeof (parentInstance: any).rootID === 'string') { // Some calls to this aren't typesafe. // This helps surface mistakes in tests. throw new Error('appendChild() first argument is not an instance.'); } appendChildToContainerOrInstance(parentInstance, child); } function insertInContainerOrInstanceBefore( parentInstance: Container | Instance, child: Instance | TextInstance, beforeChild: Instance | TextInstance, ): void { const index = parentInstance.children.indexOf(child); if (index !== -1) { parentInstance.children.splice(index, 1); } const beforeIndex = parentInstance.children.indexOf(beforeChild); if (beforeIndex === -1) { throw new Error('This child does not exist.'); } parentInstance.children.splice(beforeIndex, 0, child); } function insertInContainerBefore( parentInstance: Container, child: Instance | TextInstance, beforeChild: Instance | TextInstance, ) { if (typeof parentInstance.rootID !== 'string') { // Some calls to this aren't typesafe. // This helps surface mistakes in tests. throw new Error( 'insertInContainerBefore() first argument is not a container.', ); } insertInContainerOrInstanceBefore(parentInstance, child, beforeChild); } function insertBefore( parentInstance: Instance, child: Instance | TextInstance, beforeChild: Instance | TextInstance, ) { if (typeof (parentInstance: any).rootID === 'string') { // Some calls to this aren't typesafe. // This helps surface mistakes in tests. throw new Error('insertBefore() first argument is not an instance.'); } insertInContainerOrInstanceBefore(parentInstance, child, beforeChild); } function clearContainer(container: Container): void { container.children.splice(0); } function removeChildFromContainerOrInstance( parentInstance: Container | Instance, child: Instance | TextInstance, ): void { const index = parentInstance.children.indexOf(child); if (index === -1) { throw new Error('This child does not exist.'); } parentInstance.children.splice(index, 1); } function removeChildFromContainer( parentInstance: Container, child: Instance | TextInstance, ): void { if (typeof parentInstance.rootID !== 'string') { // Some calls to this aren't typesafe. // This helps surface mistakes in tests. throw new Error( 'removeChildFromContainer() first argument is not a container.', ); } removeChildFromContainerOrInstance(parentInstance, child); } function removeChild( parentInstance: Instance, child: Instance | TextInstance, ): void { if (typeof (parentInstance: any).rootID === 'string') { // Some calls to this aren't typesafe. // This helps surface mistakes in tests. throw new Error('removeChild() first argument is not an instance.'); } removeChildFromContainerOrInstance(parentInstance, child); } function cloneInstance( instance: Instance, type: string, oldProps: Props, newProps: Props, keepChildren: boolean, children: ?$ReadOnlyArray<Instance>, ): Instance { if (__DEV__) { checkPropStringCoercion(newProps.children, 'children'); } const clone = { id: instance.id, type: type, parent: instance.parent, children: keepChildren ? instance.children : (children ?? []), text: shouldSetTextContent(type, newProps) ? computeText((newProps.children: any) + '', instance.context) : null, prop: newProps.prop, hidden: !!newProps.hidden, context: instance.context, }; if (type === 'suspensey-thing' && typeof newProps.src === 'string') { clone.src = newProps.src; } Object.defineProperty(clone, 'id', { value: clone.id, enumerable: false, }); Object.defineProperty(clone, 'parent', { value: clone.parent, enumerable: false, }); Object.defineProperty(clone, 'text', { value: clone.text, enumerable: false, }); Object.defineProperty(clone, 'context', { value: clone.context, enumerable: false, }); hostCloneCounter++; return clone; } function shouldSetTextContent(type: string, props: Props): boolean { if (type === 'errorInBeginPhase') { throw new Error('Error in host config.'); } return ( typeof props.children === 'string' || typeof props.children === 'number' || typeof props.children === 'bigint' ); } function computeText(rawText, hostContext) { return hostContext === UPPERCASE_CONTEXT ? rawText.toUpperCase() : rawText; } type SuspenseyThingRecord = { status: 'pending' | 'fulfilled', subscriptions: Array<SuspenseyCommitSubscription> | null, }; let suspenseyThingCache: Map< SuspenseyThingRecord, 'pending' | 'fulfilled', > | null = null; function startSuspendingCommit(): SuspendedState { // Represents a subscription for all the suspensey things that block a // particular commit. Once they've all loaded, the commit phase can proceed. return { pendingCount: 0, commit: null, }; } function suspendInstance( state: SuspendedState, instance: Instance, type: string, props: Props, ): void { const src = props.src; if (type === 'suspensey-thing' && typeof src === 'string') { // Attach a listener to the suspensey thing and create a subscription // object that uses reference counting to track when all the suspensey // things have loaded. const record = suspenseyThingCache.get(src); if (record === undefined) { throw new Error('Could not find record for key.'); } if (record.status === 'fulfilled') { // Already loaded. } else if (record.status === 'pending') { state.pendingCount++; // Stash the subscription on the record. In `resolveSuspenseyThing`, // we'll use this fire the commit once all the things have loaded. if (record.subscriptions === null) { record.subscriptions = []; } record.subscriptions.push(state); } } else { throw new Error( 'Did not expect this host component to be visited when suspending ' + 'the commit. Did you check the SuspendCommit flag?', ); } } function waitForCommitToBeReady( state: SuspendedState, timeoutOffset: number, ): ((commit: () => mixed) => () => void) | null { if (state.pendingCount > 0) { return (commit: () => void) => { state.commit = commit; const cancelCommit = () => { state.commit = null; }; return cancelCommit; }; } return null; } const sharedHostConfig = { rendererVersion: ReactVersion, rendererPackageName: 'react-noop', supportsSingletons: false, getRootHostContext() { return NO_CONTEXT; }, getChildHostContext(parentHostContext: HostContext, type: string) { if (type === 'offscreen') { return parentHostContext; } if (type === 'uppercase') { return UPPERCASE_CONTEXT; } return NO_CONTEXT; }, getPublicInstance(instance) { return instance; }, createInstance( type: string, props: Props, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: Object, ): Instance { if (type === 'errorInCompletePhase') { throw new Error('Error in host config.'); } if (__DEV__) { // The `if` statement here prevents auto-disabling of the safe coercion // ESLint rule, so we must manually disable it below. if (shouldSetTextContent(type, props)) { checkPropStringCoercion(props.children, 'children'); } } const inst = { id: instanceCounter++, type: type, children: [], parent: -1, text: shouldSetTextContent(type, props) ? // eslint-disable-next-line react-internal/safe-string-coercion computeText((props.children: any) + '', hostContext) : null, prop: props.prop, hidden: !!props.hidden, context: hostContext, }; if (type === 'suspensey-thing' && typeof props.src === 'string') { inst.src = props.src; } // Hide from unit tests Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false}); Object.defineProperty(inst, 'parent', { value: inst.parent, enumerable: false, }); Object.defineProperty(inst, 'text', { value: inst.text, enumerable: false, }); Object.defineProperty(inst, 'context', { value: inst.context, enumerable: false, }); Object.defineProperty(inst, 'fiber', { value: internalInstanceHandle, enumerable: false, }); return inst; }, cloneMutableInstance(instance: Instance, keepChildren: boolean): Instance { throw new Error('Not yet implemented.'); }, appendInitialChild( parentInstance: Instance, child: Instance | TextInstance, ): void { const prevParent = child.parent; if (prevParent !== -1 && prevParent !== parentInstance.id) { throw new Error('Reparenting is not allowed'); } child.parent = parentInstance.id; parentInstance.children.push(child); }, finalizeInitialChildren( domElement: Instance, type: string, props: Props, ): boolean { return false; }, shouldSetTextContent, createTextInstance( text: string, rootContainerInstance: Container, hostContext: Object, internalInstanceHandle: Object, ): TextInstance { if (hostContext === UPPERCASE_CONTEXT) { text = text.toUpperCase(); } const inst = { text: text, id: instanceCounter++, parent: -1, hidden: false, context: hostContext, }; // Hide from unit tests Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false}); Object.defineProperty(inst, 'parent', { value: inst.parent, enumerable: false, }); Object.defineProperty(inst, 'context', { value: inst.context, enumerable: false, }); return inst; }, cloneMutableTextInstance(textInstance: TextInstance): TextInstance { throw new Error('Not yet implemented.'); }, createFragmentInstance(fragmentFiber) { return null; }, updateFragmentInstanceFiber(fragmentFiber, fragmentInstance) { // Noop }, commitNewChildToFragmentInstance(child, fragmentInstance) { // Noop }, deleteChildFromFragmentInstance(child, fragmentInstance) { // Noop }, scheduleTimeout: setTimeout, cancelTimeout: clearTimeout, noTimeout: -1, supportsMicrotasks: true, scheduleMicrotask: typeof queueMicrotask === 'function' ? queueMicrotask : typeof Promise !== 'undefined' ? callback => Promise.resolve(null) .then(callback) .catch(error => { setTimeout(() => { throw error; }); }) : setTimeout, prepareForCommit(): null | Object { return null; }, resetAfterCommit(): void {}, setCurrentUpdatePriority, getCurrentUpdatePriority, resolveUpdatePriority() { if (currentUpdatePriority !== NoEventPriority) { return currentUpdatePriority; } return currentEventPriority; }, trackSchedulerEvent(): void {}, resolveEventType(): null | string { return null; }, resolveEventTimeStamp(): number { return -1.1; }, shouldAttemptEagerTransition(): boolean { return false; }, now: Scheduler.unstable_now, isPrimaryRenderer: true, warnsIfNotActing: true, supportsHydration: false, getInstanceFromNode() { throw new Error('Not yet implemented.'); }, beforeActiveInstanceBlur() { // NO-OP }, afterActiveInstanceBlur() { // NO-OP }, preparePortalMount() { // NO-OP }, prepareScopeUpdate() {}, getInstanceFromScope() { throw new Error('Not yet implemented.'); }, detachDeletedInstance() {}, logRecoverableError() { // no-op }, requestPostPaintCallback(callback) { const endTime = Scheduler.unstable_now(); callback(endTime); }, maySuspendCommit(type: string, props: Props): boolean { // Asks whether it's possible for this combination of type and props // to ever need to suspend. This is different from asking whether it's // currently ready because even if it's ready now, it might get purged // from the cache later. return type === 'suspensey-thing' && typeof props.src === 'string'; }, maySuspendCommitOnUpdate( type: string, oldProps: Props, newProps: Props, ): boolean { // Asks whether it's possible for this combination of type and props // to ever need to suspend. This is different from asking whether it's // currently ready because even if it's ready now, it might get purged // from the cache later. return ( type === 'suspensey-thing' && typeof newProps.src === 'string' && newProps.src !== oldProps.src ); }, maySuspendCommitInSyncRender(type: string, props: Props): boolean { return true; }, mayResourceSuspendCommit(resource: mixed): boolean { throw new Error( 'Resources are not implemented for React Noop yet. This method should not be called', ); }, preloadInstance(instance: Instance, type: string, props: Props): boolean { if (type !== 'suspensey-thing' || typeof props.src !== 'string') { throw new Error('Attempted to preload unexpected instance: ' + type); } // In addition to preloading an instance, this method asks whether the // instance is ready to be committed. If it's not, React may yield to the // main thread and ask again. It's possible a load event will fire in // between, in which case we can avoid showing a fallback. if (suspenseyThingCache === null) { suspenseyThingCache = new Map(); } const record = suspenseyThingCache.get(props.src); if (record === undefined) { const newRecord: SuspenseyThingRecord = { status: 'pending', subscriptions: null, }; suspenseyThingCache.set(props.src, newRecord); const onLoadStart = props.onLoadStart; if (typeof onLoadStart === 'function') { onLoadStart(); } return false; } else { return record.status === 'fulfilled'; } }, preloadResource(resource: mixed): number { throw new Error( 'Resources are not implemented for React Noop yet. This method should not be called', ); }, startSuspendingCommit, suspendInstance, suspendResource(state: SuspendedState, resource: mixed): void { throw new Error( 'Resources are not implemented for React Noop yet. This method should not be called', ); }, suspendOnActiveViewTransition( state: SuspendedState, container: Container, ): void { // Not implemented }, waitForCommitToBeReady, getSuspendedCommitReason( state: SuspendedState, rootContainer: Container, ): null | string { return null; }, NotPendingTransition: (null: TransitionStatus), resetFormInstance(form: Instance) {}, bindToConsole(methodName, args, badgeName) { return Function.prototype.bind.apply( // eslint-disable-next-line react-internal/no-production-logging console[methodName], [console].concat(args), ); }, }; const hostConfig = useMutation ? { ...sharedHostConfig, supportsMutation: true, supportsPersistence: false, commitMount(instance: Instance, type: string, newProps: Props): void { // Noop }, commitUpdate( instance: Instance, type: string, oldProps: Props, newProps: Props, ): void { if (oldProps === null) { throw new Error('Should have old props'); } hostUpdateCounter++; instance.prop = newProps.prop; instance.hidden = !!newProps.hidden; if (type === 'suspensey-thing' && typeof newProps.src === 'string') { instance.src = newProps.src; } if (shouldSetTextContent(type, newProps)) { if (__DEV__) { checkPropStringCoercion(newProps.children, 'children'); } instance.text = computeText( (newProps.children: any) + '', instance.context, ); } }, commitTextUpdate( textInstance: TextInstance, oldText: string, newText: string, ): void { hostUpdateCounter++; textInstance.text = computeText(newText, textInstance.context); }, appendChild, appendChildToContainer, insertBefore, insertInContainerBefore, removeChild, removeChildFromContainer, clearContainer, hideInstance(instance: Instance): void { instance.hidden = true; }, hideTextInstance(textInstance: TextInstance): void { textInstance.hidden = true; }, unhideInstance(instance: Instance, props: Props): void { if (!props.hidden) { instance.hidden = false; } }, unhideTextInstance(textInstance: TextInstance, text: string): void { textInstance.hidden = false; }, applyViewTransitionName( instance: Instance, name: string, className: ?string, ): void {}, restoreViewTransitionName(instance: Instance, props: Props): void {}, cancelViewTransitionName( instance: Instance, name: string, props: Props, ): void {}, cancelRootViewTransitionName(rootContainer: Container): void {}, restoreRootViewTransitionName(rootContainer: Container): void {}, cloneRootViewTransitionContainer(rootContainer: Container): Instance { throw new Error('Not yet implemented.'); }, removeRootViewTransitionClone( rootContainer: Container, clone: Instance, ): void { throw new Error('Not implemented.'); }, measureInstance(instance: Instance): InstanceMeasurement { return null; }, measureClonedInstance(instance: Instance): InstanceMeasurement { return null; }, wasInstanceInViewport(measurement: InstanceMeasurement): boolean { return true; }, hasInstanceChanged( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { return false; }, hasInstanceAffectedParent( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { return false; }, startViewTransition( rootContainer: Container, transitionTypes: null | TransitionTypes, mutationCallback: () => void, layoutCallback: () => void, afterMutationCallback: () => void, spawnedWorkCallback: () => void, passiveCallback: () => mixed, errorCallback: mixed => void, blockedCallback: string => void, // Profiling-only finishedAnimation: () => void, // Profiling-only ): null | RunningViewTransition { mutationCallback(); layoutCallback(); // Skip afterMutationCallback(). We don't need it since we're not animating. spawnedWorkCallback(); // Skip passiveCallback(). Spawned work will schedule a task. return null; }, startGestureTransition( rootContainer: Container, timeline: GestureTimeline, rangeStart: number, rangeEnd: number, transitionTypes: null | TransitionTypes, mutationCallback: () => void, animateCallback: () => void, errorCallback: mixed => void, ): null | RunningViewTransition { mutationCallback(); animateCallback(); return null; }, stopViewTransition(transition: RunningViewTransition) {}, createViewTransitionInstance(name: string): ViewTransitionInstance { return null; }, getCurrentGestureOffset(provider: GestureTimeline): number { return 0; }, resetTextContent(instance: Instance): void { instance.text = null; }, } : { ...sharedHostConfig, supportsMutation: false, supportsPersistence: true, cloneInstance, clearContainer, createContainerChildSet(): Array<Instance | TextInstance> { return []; }, appendChildToContainerChildSet( childSet: Array<Instance | TextInstance>, child: Instance | TextInstance, ): void { childSet.push(child); }, finalizeContainerChildren( container: Container, newChildren: Array<Instance | TextInstance>, ): void { container.pendingChildren = newChildren; if ( newChildren.length === 1 && newChildren[0].text === 'Error when completing root' ) { // Trigger an error for testing purposes throw Error('Error when completing root'); } }, replaceContainerChildren( container: Container, newChildren: Array<Instance | TextInstance>, ): void { container.children = newChildren; }, cloneHiddenInstance( instance: Instance, type: string, props: Props, ): Instance { const clone = cloneInstance(instance, type, props, props, true, null); clone.hidden = true; return clone; }, cloneHiddenTextInstance( instance: TextInstance, text: string, ): TextInstance { const clone = { text: instance.text, id: instance.id, parent: instance.parent, hidden: true, context: instance.context, }; // Hide from unit tests Object.defineProperty(clone, 'id', { value: clone.id, enumerable: false, }); Object.defineProperty(clone, 'parent', { value: clone.parent, enumerable: false, }); Object.defineProperty(clone, 'context', { value: clone.context, enumerable: false, }); return clone; }, }; const NoopRenderer = reconciler(hostConfig); const rootContainers = new Map(); const roots = new Map(); const DEFAULT_ROOT_ID = '<default>'; let currentUpdatePriority = NoEventPriority; function setCurrentUpdatePriority(newPriority: EventPriority): void { currentUpdatePriority = newPriority; } function getCurrentUpdatePriority(): EventPriority { return currentUpdatePriority; } let currentEventPriority = DefaultEventPriority; function createJSXElementForTestComparison(type, props) { if (__DEV__) { const element = { type: type, $$typeof: REACT_ELEMENT_TYPE, key: null, props: props, _owner: null, _store: __DEV__ ? {} : undefined, }; Object.defineProperty(element, 'ref', { enumerable: false, value: null, }); return element; } else { return { $$typeof: REACT_ELEMENT_TYPE, type: type, key: null, ref: null, props: props, }; } } function childToJSX(child, text) { if (text !== null) { return text; } if (child === null) { return null; } if (typeof child === 'string') { return child; } if (isArray(child)) { if (child.length === 0) { return null; } if (child.length === 1) { return childToJSX(child[0], null); } const children = child.map(c => childToJSX(c, null)); if ( children.every( c => typeof c === 'string' || typeof c === 'number' || typeof c === 'bigint', ) ) { return children.join(''); } return children; } if (isArray(child.children)) { // This is an instance. const instance: Instance = (child: any); const children = childToJSX(instance.children, instance.text); const props = ({prop: instance.prop}: any); if (instance.hidden) { props.hidden = true; } if (instance.src) { props.src = instance.src; } if (children !== null) { props.children = children; } return createJSXElementForTestComparison(instance.type, props); } // This is a text instance const textInstance: TextInstance = (child: any); if (textInstance.hidden) { return ''; } return textInstance.text; } function getChildren(root) { if (root) { return root.children; } else { return null; } } function getPendingChildren(root) { if (root) { return root.children; } else { return null; } } function getChildrenAsJSX(root) { const children = childToJSX(getChildren(root), null); if (children === null) { return null; } if (isArray(children)) { return createJSXElementForTestComparison(REACT_FRAGMENT_TYPE, {children}); } return children; } function getPendingChildrenAsJSX(root) { const children = childToJSX(getChildren(root), null); if (children === null) { return null; } if (isArray(children)) { return createJSXElementForTestComparison(REACT_FRAGMENT_TYPE, {children}); } return children; } function flushSync<R>(fn: () => R): R { if (__DEV__) { if (NoopRenderer.isAlreadyRendering()) { console.error( 'flushSync was called from inside a lifecycle method. React cannot ' + 'flush when React is already rendering. Consider moving this call to ' + 'a scheduler task or micro task.', ); } } if (disableLegacyMode) { const previousTransition = ReactSharedInternals.T; const preivousEventPriority = currentEventPriority; try { ReactSharedInternals.T = null; currentEventPriority = DiscreteEventPriority; if (fn) { return fn(); } else { return undefined; } } finally { ReactSharedInternals.T = previousTransition; currentEventPriority = preivousEventPriority; NoopRenderer.flushSyncWork(); } } else { return NoopRenderer.flushSyncFromReconciler(fn); } } function onRecoverableError(error) { // TODO: Turn this on once tests are fixed // console.error(error); } function onDefaultTransitionIndicator(): void | (() => void) {} let idCounter = 0; const ReactNoop = { _Scheduler: Scheduler, getChildren(rootID: string = DEFAULT_ROOT_ID) { throw new Error( 'No longer supported due to bad performance when used with `expect()`. ' + 'Use `ReactNoop.getChildrenAsJSX()` instead or, if you really need to, `dangerouslyGetChildren` after you carefully considered the warning in its JSDOC.', ); }, getPendingChildren(rootID: string = DEFAULT_ROOT_ID) { throw new Error( 'No longer supported due to bad performance when used with `expect()`. ' + 'Use `ReactNoop.getPendingChildrenAsJSX()` instead or, if you really need to, `dangerouslyGetPendingChildren` after you carefully considered the warning in its JSDOC.', ); }, /** * Prefer using `getChildrenAsJSX`. * Using the returned children in `.toEqual` has very poor performance on mismatch due to deep equality checking of fiber structures. * Make sure you deeply remove enumerable properties before passing it to `.toEqual`, or, better, use `getChildrenAsJSX` or `toMatchRenderedOutput`. */ dangerouslyGetChildren(rootID: string = DEFAULT_ROOT_ID) { const container = rootContainers.get(rootID); return getChildren(container); }, /** * Prefer using `getPendingChildrenAsJSX`. * Using the returned children in `.toEqual` has very poor performance on mismatch due to deep equality checking of fiber structures. * Make sure you deeply remove enumerable properties before passing it to `.toEqual`, or, better, use `getChildrenAsJSX` or `toMatchRenderedOutput`. */ dangerouslyGetPendingChildren(rootID: string = DEFAULT_ROOT_ID) { const container = rootContainers.get(rootID); return getPendingChildren(container); }, getOrCreateRootContainer(rootID: string = DEFAULT_ROOT_ID, tag: RootTag) { let root = roots.get(rootID); if (!root) { const container = {rootID: rootID, pendingChildren: [], children: []}; rootContainers.set(rootID, container); root = NoopRenderer.createContainer( container, tag, null, null, false, '', NoopRenderer.defaultOnUncaughtError, NoopRenderer.defaultOnCaughtError, onRecoverableError, onDefaultTransitionIndicator, null, ); roots.set(rootID, root); } return root.current.stateNode.containerInfo; }, // TODO: Replace ReactNoop.render with createRoot + root.render createRoot(options?: CreateRootOptions) { const container = { rootID: '' + idCounter++, pendingChildren: [], children: [], }; const fiberRoot = NoopRenderer.createContainer( container, ConcurrentRoot, null, null, false, '', options && options.onUncaughtError ? options.onUncaughtError : NoopRenderer.defaultOnUncaughtError, options && options.onCaughtError ? options.onCaughtError : NoopRenderer.defaultOnCaughtError, onRecoverableError, options && options.onDefaultTransitionIndicator ? options.onDefaultTransitionIndicator : onDefaultTransitionIndicator, options && options.unstable_transitionCallbacks ? options.unstable_transitionCallbacks : null, ); return { _Scheduler: Scheduler, render(children: ReactNodeList) { NoopRenderer.updateContainer(children, fiberRoot, null, null); }, getChildren() { return getChildren(container); }, getChildrenAsJSX() { return getChildrenAsJSX(container); }, }; }, createLegacyRoot() { if (disableLegacyMode) { throw new Error('createLegacyRoot: Unsupported Legacy Mode API.'); } const container = { rootID: '' + idCounter++, pendingChildren: [], children: [], }; const fiberRoot = NoopRenderer.createContainer( container, LegacyRoot, null, null, false, '', NoopRenderer.defaultOnUncaughtError, NoopRenderer.defaultOnCaughtError, onRecoverableError, onDefaultTransitionIndicator, null, ); return { _Scheduler: Scheduler, render(children: ReactNodeList) { NoopRenderer.updateContainer(children, fiberRoot, null, null); }, getChildren() { return getChildren(container); }, getChildrenAsJSX() { return getChildrenAsJSX(container); }, legacy: true, }; }, getChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) { const container = rootContainers.get(rootID); return getChildrenAsJSX(container); }, getPendingChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) { const container = rootContainers.get(rootID); return getPendingChildrenAsJSX(container); }, getSuspenseyThingStatus(src): string | null { if (suspenseyThingCache === null) { return null; } else { const record = suspenseyThingCache.get(src); return record === undefined ? null : record.status; } }, resolveSuspenseyThing(key: string): void { if (suspenseyThingCache === null) { suspenseyThingCache = new Map(); } const record = suspenseyThingCache.get(key); if (record === undefined) { const newRecord: SuspenseyThingRecord = { status: 'fulfilled', subscriptions: null, }; suspenseyThingCache.set(key, newRecord); } else { if (record.status === 'pending') { record.status = 'fulfilled'; const subscriptions = record.subscriptions; if (subscriptions !== null) { record.subscriptions = null; for (let i = 0; i < subscriptions.length; i++) { const subscription = subscriptions[i]; subscription.pendingCount--; if (subscription.pendingCount === 0) { const commit = subscription.commit; subscription.commit = null; commit(); } } } } } }, resetSuspenseyThingCache() { suspenseyThingCache = null; }, createPortal( children: ReactNodeList, container: Container, key: ?string = null, ) { return NoopRenderer.createPortal(children, container, null, key); }, // Shortcut for testing a single root render(element: React$Element<any>, callback: ?Function) { ReactNoop.renderToRootWithID(element, DEFAULT_ROOT_ID, callback); }, renderLegacySyncRoot(element: React$Element<any>, callback: ?Function) { if (disableLegacyMode) { throw new Error('createLegacyRoot: Unsupported Legacy Mode API.'); } const rootID = DEFAULT_ROOT_ID; const container = ReactNoop.getOrCreateRootContainer(rootID, LegacyRoot); const root = roots.get(container.rootID); NoopRenderer.updateContainer(element, root, null, callback); }, renderToRootWithID( element: React$Element<any>, rootID: string, callback: ?Function, ) { const container = ReactNoop.getOrCreateRootContainer( rootID, ConcurrentRoot, ); const root = roots.get(container.rootID); NoopRenderer.updateContainer(element, root, null, callback); }, unmountRootWithID(rootID: string) { const root = roots.get(rootID); if (root) { NoopRenderer.updateContainer(null, root, null, () => { roots.delete(rootID); rootContainers.delete(rootID); }); } }, findInstance( componentOrElement: Element | ?component(...props: any), ): null | Instance | TextInstance { if (componentOrElement == null) { return null; } // Unsound duck typing. const component = (componentOrElement: any); if (typeof component.id === 'number') { return component; } if (__DEV__) { return NoopRenderer.findHostInstanceWithWarning( component, 'findInstance', ); } return NoopRenderer.findHostInstance(component); }, flushNextYield(): Array<mixed> { Scheduler.unstable_flushNumberOfYields(1); return Scheduler.unstable_clearLog(); }, startTrackingHostCounters(): void { hostUpdateCounter = 0; hostCloneCounter = 0; }, stopTrackingHostCounters(): | { hostUpdateCounter: number, } | { hostCloneCounter: number, } { const result = useMutation ? { hostUpdateCounter, } : { hostCloneCounter, }; hostUpdateCounter = 0; hostCloneCounter = 0; return result; }, expire: Scheduler.unstable_advanceTime, flushExpired(): Array<mixed> { return Scheduler.unstable_flushExpired(); }, unstable_runWithPriority: function runWithPriority<T>( priority: EventPriority, fn: () => T, ): T { const previousPriority = getCurrentUpdatePriority(); try { setCurrentUpdatePriority(priority); return fn(); } finally { setCurrentUpdatePriority(previousPriority); } }, batchedUpdates: NoopRenderer.batchedUpdates, deferredUpdates: NoopRenderer.deferredUpdates, discreteUpdates: NoopRenderer.discreteUpdates, idleUpdates<T>(fn: () => T): T { const prevEventPriority = currentEventPriority; currentEventPriority = IdleEventPriority; try { fn(); } finally { currentEventPriority = prevEventPriority; } }, flushSync, flushPassiveEffects: NoopRenderer.flushPassiveEffects, // Logs the current state of the tree. dumpTree(rootID: string = DEFAULT_ROOT_ID) { const root = roots.get(rootID); const rootContainer = rootContainers.get(rootID); if (!root || !rootContainer) { // eslint-disable-next-line react-internal/no-production-logging console.log('Nothing rendered yet.'); return; } const bufferedLog = []; function log(...args) { bufferedLog.push(...args, '\n'); } function logHostInstances( children: Array<Instance | TextInstance>, depth, ) { for (let i = 0; i < children.length; i++) { const child = children[i]; const indent = ' '.repeat(depth); if (typeof child.text === 'string') { log(indent + '- ' + child.text); } else { log(indent + '- ' + child.type + '#' + child.id); logHostInstances(child.children, depth + 1); } } } function logContainer(container: Container, depth) { log(' '.repeat(depth) + '- [root#' + container.rootID + ']'); logHostInstances(container.children, depth + 1); } function logUpdateQueue(updateQueue: UpdateQueue<mixed>, depth) { log(' '.repeat(depth + 1) + 'QUEUED UPDATES'); const first = updateQueue.firstBaseUpdate; const update = first; if (update !== null) { do { log( ' '.repeat(depth + 1) + '~', '[' + update.expirationTime + ']', ); } while (update !== null); } const lastPending = updateQueue.shared.pending; if (lastPending !== null) { const firstPending = lastPending.next; const pendingUpdate = firstPending; if (pendingUpdate !== null) { do { log( ' '.repeat(depth + 1) + '~', '[' + pendingUpdate.expirationTime + ']', ); } while (pendingUpdate !== null && pendingUpdate !== firstPending); } } } function logFiber(fiber: Fiber, depth) { log( ' '.repeat(depth) + '- ' + // need to explicitly coerce Symbol to a string (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'), '[' + fiber.childExpirationTime + (fiber.pendingProps ? '*' : '') + ']', ); if (fiber.updateQueue) { logUpdateQueue(fiber.updateQueue, depth); } // const childInProgress = fiber.progressedChild; // if (childInProgress && childInProgress !== fiber.child) { // log( // ' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.pendingWorkPriority, // ); // logFiber(childInProgress, depth + 1); // if (fiber.child) { // log(' '.repeat(depth + 1) + 'CURRENT'); // } // } else if (fiber.child && fiber.updateQueue) { // log(' '.repeat(depth + 1) + 'CHILDREN'); // } if (fiber.child) { logFiber(fiber.child, depth + 1); } if (fiber.sibling) { logFiber(fiber.sibling, depth); } } log('HOST INSTANCES:'); logContainer(rootContainer, 0); log('FIBERS:'); logFiber(root.current, 0); // eslint-disable-next-line react-internal/no-production-logging console.log(...bufferedLog); }, getRoot(rootID: string = DEFAULT_ROOT_ID) { return roots.get(rootID); }, }; return ReactNoop; } export default createReactNoop;