From d9fb383d6f627bf3f6e53ec7b14ba4d1260959d5 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 28 Sep 2021 15:05:44 -0400 Subject: [PATCH 001/109] Extract queueing logic into shared functions (#22452) As a follow up to #22445, this extracts the queueing logic that is shared between `dispatchSetState` and `dispatchReducerAction` into separate functions. It likely doesn't save any bytes since these will get inlined, anyway, but it does make the flow a bit easier to follow. --- .../src/ReactFiberHooks.new.js | 246 ++++++++---------- .../src/ReactFiberHooks.old.js | 246 ++++++++---------- 2 files changed, 214 insertions(+), 278 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 6d7cb75bbd751..d6123454972ac 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -113,9 +113,9 @@ import {logStateUpdateScheduled} from './DebugTracing'; import {markStateUpdateScheduled} from './SchedulingProfiler'; import {CacheContext} from './ReactFiberCacheComponent.new'; import { - createUpdate, - enqueueUpdate, - entangleTransitions, + createUpdate as createLegacyQueueUpdate, + enqueueUpdate as enqueueLegacyQueueUpdate, + entangleTransitions as entangleLegacyQueueTransitions, } from './ReactUpdateQueue.new'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; @@ -2125,7 +2125,7 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(provider, lane, eventTime); if (root !== null) { - entangleTransitions(root, provider, lane); + entangleLegacyQueueTransitions(root, provider, lane); } const seededCache = new Map(); @@ -2136,12 +2136,12 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { } // Schedule an update on the cache boundary to trigger a refresh. - const refreshUpdate = createUpdate(eventTime, lane); + const refreshUpdate = createLegacyQueueUpdate(eventTime, lane); const payload = { cache: seededCache, }; refreshUpdate.payload = payload; - enqueueUpdate(provider, refreshUpdate, lane); + enqueueLegacyQueueUpdate(provider, refreshUpdate, lane); return; } } @@ -2165,7 +2165,6 @@ function dispatchReducerAction( } } - const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); const update: Update = { @@ -2176,49 +2175,10 @@ function dispatchReducerAction( next: (null: any), }; - const alternate = fiber.alternate; - if ( - fiber === currentlyRenderingFiber || - (alternate !== null && alternate === currentlyRenderingFiber) - ) { - // This is a render phase update. Stash it in a lazily-created map of - // queue -> linked list of updates. After this render pass, we'll restart - // and apply the stashed updates on top of the work-in-progress hook. - didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; - const pending = queue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - queue.pending = update; + if (isRenderPhaseUpdate(fiber)) { + enqueueRenderPhaseUpdate(queue, update); } else { - if (isInterleavedUpdate(fiber, lane)) { - const interleaved = queue.interleaved; - if (interleaved === null) { - // This is the first update. Create a circular list. - update.next = update; - // At the end of the current render, this queue's interleaved updates will - // be transferred to the pending queue. - pushInterleavedQueue(queue); - } else { - update.next = interleaved.next; - interleaved.next = update; - } - queue.interleaved = update; - } else { - const pending = queue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - queue.pending = update; - } + enqueueUpdate(fiber, queue, update, lane); if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests @@ -2226,40 +2186,14 @@ function dispatchReducerAction( warnIfNotCurrentlyActingUpdatesInDev(fiber); } } + const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); - - if (isTransitionLane(lane) && root !== null) { - let queueLanes = queue.lanes; - - // If any entangled lanes are no longer pending on the root, then they - // must have finished. We can remove them from the shared queue, which - // represents a superset of the actually pending lanes. In some cases we - // may entangle more than we need to, but that's OK. In fact it's worse if - // we *don't* entangle when we should. - queueLanes = intersectLanes(queueLanes, root.pendingLanes); - - // Entangle the new transition lane with the other transition lanes. - const newQueueLanes = mergeLanes(queueLanes, lane); - queue.lanes = newQueueLanes; - // Even if queue.lanes already include lane, we don't know for certain if - // the lane finished since the last time we entangled it. So we need to - // entangle it again, just to be sure. - markRootEntangled(root, newQueueLanes); - } - } - - if (__DEV__) { - if (enableDebugTracing) { - if (fiber.mode & DebugTracingMode) { - const name = getComponentNameFromFiber(fiber) || 'Unknown'; - logStateUpdateScheduled(name, lane, action); - } + if (root !== null) { + entangleTransitionUpdate(root, queue, lane); } } - if (enableSchedulingProfiler) { - markStateUpdateScheduled(fiber, lane); - } + markUpdateInDevTools(fiber, lane, action); } function dispatchSetState( @@ -2277,7 +2211,6 @@ function dispatchSetState( } } - const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); const update: Update = { @@ -2288,50 +2221,12 @@ function dispatchSetState( next: (null: any), }; - const alternate = fiber.alternate; - if ( - fiber === currentlyRenderingFiber || - (alternate !== null && alternate === currentlyRenderingFiber) - ) { - // This is a render phase update. Stash it in a lazily-created map of - // queue -> linked list of updates. After this render pass, we'll restart - // and apply the stashed updates on top of the work-in-progress hook. - didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; - const pending = queue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - queue.pending = update; + if (isRenderPhaseUpdate(fiber)) { + enqueueRenderPhaseUpdate(queue, update); } else { - if (isInterleavedUpdate(fiber, lane)) { - const interleaved = queue.interleaved; - if (interleaved === null) { - // This is the first update. Create a circular list. - update.next = update; - // At the end of the current render, this queue's interleaved updates will - // be transferred to the pending queue. - pushInterleavedQueue(queue); - } else { - update.next = interleaved.next; - interleaved.next = update; - } - queue.interleaved = update; - } else { - const pending = queue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - queue.pending = update; - } + enqueueUpdate(fiber, queue, update, lane); + const alternate = fiber.alternate; if ( fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes) @@ -2377,28 +2272,101 @@ function dispatchSetState( warnIfNotCurrentlyActingUpdatesInDev(fiber); } } + const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + if (root !== null) { + entangleTransitionUpdate(root, queue, lane); + } + } + + markUpdateInDevTools(fiber, lane, action); +} + +function isRenderPhaseUpdate(fiber: Fiber) { + const alternate = fiber.alternate; + return ( + fiber === currentlyRenderingFiber || + (alternate !== null && alternate === currentlyRenderingFiber) + ); +} + +function enqueueRenderPhaseUpdate( + queue: UpdateQueue, + update: Update, +) { + // This is a render phase update. Stash it in a lazily-created map of + // queue -> linked list of updates. After this render pass, we'll restart + // and apply the stashed updates on top of the work-in-progress hook. + didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; + const pending = queue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; + } + queue.pending = update; +} - if (isTransitionLane(lane) && root !== null) { - let queueLanes = queue.lanes; - - // If any entangled lanes are no longer pending on the root, then they - // must have finished. We can remove them from the shared queue, which - // represents a superset of the actually pending lanes. In some cases we - // may entangle more than we need to, but that's OK. In fact it's worse if - // we *don't* entangle when we should. - queueLanes = intersectLanes(queueLanes, root.pendingLanes); - - // Entangle the new transition lane with the other transition lanes. - const newQueueLanes = mergeLanes(queueLanes, lane); - queue.lanes = newQueueLanes; - // Even if queue.lanes already include lane, we don't know for certain if - // the lane finished since the last time we entangled it. So we need to - // entangle it again, just to be sure. - markRootEntangled(root, newQueueLanes); +function enqueueUpdate( + fiber: Fiber, + queue: UpdateQueue, + update: Update, + lane: Lane, +) { + if (isInterleavedUpdate(fiber, lane)) { + const interleaved = queue.interleaved; + if (interleaved === null) { + // This is the first update. Create a circular list. + update.next = update; + // At the end of the current render, this queue's interleaved updates will + // be transferred to the pending queue. + pushInterleavedQueue(queue); + } else { + update.next = interleaved.next; + interleaved.next = update; + } + queue.interleaved = update; + } else { + const pending = queue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; } + queue.pending = update; + } +} + +function entangleTransitionUpdate( + root: FiberRoot, + queue: UpdateQueue, + lane: Lane, +) { + if (isTransitionLane(lane)) { + let queueLanes = queue.lanes; + + // If any entangled lanes are no longer pending on the root, then they + // must have finished. We can remove them from the shared queue, which + // represents a superset of the actually pending lanes. In some cases we + // may entangle more than we need to, but that's OK. In fact it's worse if + // we *don't* entangle when we should. + queueLanes = intersectLanes(queueLanes, root.pendingLanes); + + // Entangle the new transition lane with the other transition lanes. + const newQueueLanes = mergeLanes(queueLanes, lane); + queue.lanes = newQueueLanes; + // Even if queue.lanes already include lane, we don't know for certain if + // the lane finished since the last time we entangled it. So we need to + // entangle it again, just to be sure. + markRootEntangled(root, newQueueLanes); } +} +function markUpdateInDevTools(fiber, lane, action) { if (__DEV__) { if (enableDebugTracing) { if (fiber.mode & DebugTracingMode) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 75570d1c30969..d4dacbc8833ce 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -113,9 +113,9 @@ import {logStateUpdateScheduled} from './DebugTracing'; import {markStateUpdateScheduled} from './SchedulingProfiler'; import {CacheContext} from './ReactFiberCacheComponent.old'; import { - createUpdate, - enqueueUpdate, - entangleTransitions, + createUpdate as createLegacyQueueUpdate, + enqueueUpdate as enqueueLegacyQueueUpdate, + entangleTransitions as entangleLegacyQueueTransitions, } from './ReactUpdateQueue.old'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; @@ -2125,7 +2125,7 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(provider, lane, eventTime); if (root !== null) { - entangleTransitions(root, provider, lane); + entangleLegacyQueueTransitions(root, provider, lane); } const seededCache = new Map(); @@ -2136,12 +2136,12 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { } // Schedule an update on the cache boundary to trigger a refresh. - const refreshUpdate = createUpdate(eventTime, lane); + const refreshUpdate = createLegacyQueueUpdate(eventTime, lane); const payload = { cache: seededCache, }; refreshUpdate.payload = payload; - enqueueUpdate(provider, refreshUpdate, lane); + enqueueLegacyQueueUpdate(provider, refreshUpdate, lane); return; } } @@ -2165,7 +2165,6 @@ function dispatchReducerAction( } } - const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); const update: Update = { @@ -2176,49 +2175,10 @@ function dispatchReducerAction( next: (null: any), }; - const alternate = fiber.alternate; - if ( - fiber === currentlyRenderingFiber || - (alternate !== null && alternate === currentlyRenderingFiber) - ) { - // This is a render phase update. Stash it in a lazily-created map of - // queue -> linked list of updates. After this render pass, we'll restart - // and apply the stashed updates on top of the work-in-progress hook. - didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; - const pending = queue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - queue.pending = update; + if (isRenderPhaseUpdate(fiber)) { + enqueueRenderPhaseUpdate(queue, update); } else { - if (isInterleavedUpdate(fiber, lane)) { - const interleaved = queue.interleaved; - if (interleaved === null) { - // This is the first update. Create a circular list. - update.next = update; - // At the end of the current render, this queue's interleaved updates will - // be transferred to the pending queue. - pushInterleavedQueue(queue); - } else { - update.next = interleaved.next; - interleaved.next = update; - } - queue.interleaved = update; - } else { - const pending = queue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - queue.pending = update; - } + enqueueUpdate(fiber, queue, update, lane); if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests @@ -2226,40 +2186,14 @@ function dispatchReducerAction( warnIfNotCurrentlyActingUpdatesInDev(fiber); } } + const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); - - if (isTransitionLane(lane) && root !== null) { - let queueLanes = queue.lanes; - - // If any entangled lanes are no longer pending on the root, then they - // must have finished. We can remove them from the shared queue, which - // represents a superset of the actually pending lanes. In some cases we - // may entangle more than we need to, but that's OK. In fact it's worse if - // we *don't* entangle when we should. - queueLanes = intersectLanes(queueLanes, root.pendingLanes); - - // Entangle the new transition lane with the other transition lanes. - const newQueueLanes = mergeLanes(queueLanes, lane); - queue.lanes = newQueueLanes; - // Even if queue.lanes already include lane, we don't know for certain if - // the lane finished since the last time we entangled it. So we need to - // entangle it again, just to be sure. - markRootEntangled(root, newQueueLanes); - } - } - - if (__DEV__) { - if (enableDebugTracing) { - if (fiber.mode & DebugTracingMode) { - const name = getComponentNameFromFiber(fiber) || 'Unknown'; - logStateUpdateScheduled(name, lane, action); - } + if (root !== null) { + entangleTransitionUpdate(root, queue, lane); } } - if (enableSchedulingProfiler) { - markStateUpdateScheduled(fiber, lane); - } + markUpdateInDevTools(fiber, lane, action); } function dispatchSetState( @@ -2277,7 +2211,6 @@ function dispatchSetState( } } - const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); const update: Update = { @@ -2288,50 +2221,12 @@ function dispatchSetState( next: (null: any), }; - const alternate = fiber.alternate; - if ( - fiber === currentlyRenderingFiber || - (alternate !== null && alternate === currentlyRenderingFiber) - ) { - // This is a render phase update. Stash it in a lazily-created map of - // queue -> linked list of updates. After this render pass, we'll restart - // and apply the stashed updates on top of the work-in-progress hook. - didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; - const pending = queue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - queue.pending = update; + if (isRenderPhaseUpdate(fiber)) { + enqueueRenderPhaseUpdate(queue, update); } else { - if (isInterleavedUpdate(fiber, lane)) { - const interleaved = queue.interleaved; - if (interleaved === null) { - // This is the first update. Create a circular list. - update.next = update; - // At the end of the current render, this queue's interleaved updates will - // be transferred to the pending queue. - pushInterleavedQueue(queue); - } else { - update.next = interleaved.next; - interleaved.next = update; - } - queue.interleaved = update; - } else { - const pending = queue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - queue.pending = update; - } + enqueueUpdate(fiber, queue, update, lane); + const alternate = fiber.alternate; if ( fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes) @@ -2377,28 +2272,101 @@ function dispatchSetState( warnIfNotCurrentlyActingUpdatesInDev(fiber); } } + const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + if (root !== null) { + entangleTransitionUpdate(root, queue, lane); + } + } + + markUpdateInDevTools(fiber, lane, action); +} + +function isRenderPhaseUpdate(fiber: Fiber) { + const alternate = fiber.alternate; + return ( + fiber === currentlyRenderingFiber || + (alternate !== null && alternate === currentlyRenderingFiber) + ); +} + +function enqueueRenderPhaseUpdate( + queue: UpdateQueue, + update: Update, +) { + // This is a render phase update. Stash it in a lazily-created map of + // queue -> linked list of updates. After this render pass, we'll restart + // and apply the stashed updates on top of the work-in-progress hook. + didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; + const pending = queue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; + } + queue.pending = update; +} - if (isTransitionLane(lane) && root !== null) { - let queueLanes = queue.lanes; - - // If any entangled lanes are no longer pending on the root, then they - // must have finished. We can remove them from the shared queue, which - // represents a superset of the actually pending lanes. In some cases we - // may entangle more than we need to, but that's OK. In fact it's worse if - // we *don't* entangle when we should. - queueLanes = intersectLanes(queueLanes, root.pendingLanes); - - // Entangle the new transition lane with the other transition lanes. - const newQueueLanes = mergeLanes(queueLanes, lane); - queue.lanes = newQueueLanes; - // Even if queue.lanes already include lane, we don't know for certain if - // the lane finished since the last time we entangled it. So we need to - // entangle it again, just to be sure. - markRootEntangled(root, newQueueLanes); +function enqueueUpdate( + fiber: Fiber, + queue: UpdateQueue, + update: Update, + lane: Lane, +) { + if (isInterleavedUpdate(fiber, lane)) { + const interleaved = queue.interleaved; + if (interleaved === null) { + // This is the first update. Create a circular list. + update.next = update; + // At the end of the current render, this queue's interleaved updates will + // be transferred to the pending queue. + pushInterleavedQueue(queue); + } else { + update.next = interleaved.next; + interleaved.next = update; + } + queue.interleaved = update; + } else { + const pending = queue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; } + queue.pending = update; + } +} + +function entangleTransitionUpdate( + root: FiberRoot, + queue: UpdateQueue, + lane: Lane, +) { + if (isTransitionLane(lane)) { + let queueLanes = queue.lanes; + + // If any entangled lanes are no longer pending on the root, then they + // must have finished. We can remove them from the shared queue, which + // represents a superset of the actually pending lanes. In some cases we + // may entangle more than we need to, but that's OK. In fact it's worse if + // we *don't* entangle when we should. + queueLanes = intersectLanes(queueLanes, root.pendingLanes); + + // Entangle the new transition lane with the other transition lanes. + const newQueueLanes = mergeLanes(queueLanes, lane); + queue.lanes = newQueueLanes; + // Even if queue.lanes already include lane, we don't know for certain if + // the lane finished since the last time we entangled it. So we need to + // entangle it again, just to be sure. + markRootEntangled(root, newQueueLanes); } +} +function markUpdateInDevTools(fiber, lane, action) { if (__DEV__) { if (enableDebugTracing) { if (fiber.mode & DebugTracingMode) { From 7843b142ac804655990157a7be1e4641b4b6695f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 28 Sep 2021 18:32:09 -0400 Subject: [PATCH 002/109] [Fizz/Flight] Pass in Destination lazily to startFlowing instead of in createRequest (#22449) * Pass in Destination lazily in startFlowing instead of createRequest * Delay fatal errors until we have a destination to forward them to * Flow can now be inferred by whether there's a destination set We can drop the destination when we're not flowing since there's nothing to write to. Fatal errors now close once flowing starts back up again. * Defer fatal errors in Flight too --- .../__tests__/ReactDOMFizzServerNode-test.js | 5 +- .../src/server/ReactDOMFizzServerBrowser.js | 22 ++++--- .../src/server/ReactDOMFizzServerNode.js | 13 ++-- .../src/server/ReactDOMLegacyServerBrowser.js | 3 +- .../src/server/ReactDOMLegacyServerNode.js | 5 +- .../src/ReactNoopFlightServer.js | 3 +- .../src/ReactNoopServer.js | 3 +- .../src/ReactDOMServerFB.js | 3 +- .../src/ReactFlightDOMRelayServer.js | 3 +- .../src/ReactFlightDOMServerBrowser.js | 14 ++--- .../src/ReactFlightDOMServerNode.js | 5 +- .../src/ReactFlightNativeRelayServer.js | 4 +- packages/react-server/src/ReactFizzServer.js | 59 ++++++++++++------- .../react-server/src/ReactFlightServer.js | 56 ++++++++++++------ 14 files changed, 110 insertions(+), 88 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index 78a8374d09cc3..949ddcb04fa2d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -154,7 +154,7 @@ describe('ReactDOMFizzServer', () => { it('should error the stream when an error is thrown at the root', async () => { const reportedErrors = []; const {writable, output, completed} = getTestWritable(); - ReactDOMFizzServer.pipeToNodeWritable( + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
, @@ -166,7 +166,8 @@ describe('ReactDOMFizzServer', () => { }, ); - // The stream is errored even if we haven't started writing. + // The stream is errored once we start writing. + startWriting(); await completed; diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index 42752a34c0bde..d238201247626 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -37,7 +37,15 @@ function renderToReadableStream( children: ReactNodeList, options?: Options, ): ReadableStream { - let request; + const request = createRequest( + children, + createResponseState(options ? options.identifierPrefix : undefined), + createRootFormatContext(options ? options.namespaceURI : undefined), + options ? options.progressiveChunkSize : undefined, + options ? options.onError : undefined, + options ? options.onCompleteAll : undefined, + options ? options.onCompleteShell : undefined, + ); if (options && options.signal) { const signal = options.signal; const listener = () => { @@ -48,16 +56,6 @@ function renderToReadableStream( } const stream = new ReadableStream({ start(controller) { - request = createRequest( - children, - controller, - createResponseState(options ? options.identifierPrefix : undefined), - createRootFormatContext(options ? options.namespaceURI : undefined), - options ? options.progressiveChunkSize : undefined, - options ? options.onError : undefined, - options ? options.onCompleteAll : undefined, - options ? options.onCompleteShell : undefined, - ); startWork(request); }, pull(controller) { @@ -66,7 +64,7 @@ function renderToReadableStream( // is actually used by something so we can give it the best result possible // at that point. if (stream.locked) { - startFlowing(request); + startFlowing(request, controller); } }, cancel(reason) {}, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index 2bfb505ea0625..fb1939f0c404f 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -25,7 +25,7 @@ import { } from './ReactDOMServerFormatConfig'; function createDrainHandler(destination, request) { - return () => startFlowing(request); + return () => startFlowing(request, destination); } type Options = {| @@ -44,14 +44,9 @@ type Controls = {| startWriting(): void, |}; -function createRequestImpl( - children: ReactNodeList, - destination: Writable, - options: void | Options, -) { +function createRequestImpl(children: ReactNodeList, options: void | Options) { return createRequest( children, - destination, createResponseState(options ? options.identifierPrefix : undefined), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, @@ -66,7 +61,7 @@ function pipeToNodeWritable( destination: Writable, options?: Options, ): Controls { - const request = createRequestImpl(children, destination, options); + const request = createRequestImpl(children, options); let hasStartedFlowing = false; startWork(request); return { @@ -75,7 +70,7 @@ function pipeToNodeWritable( return; } hasStartedFlowing = true; - startFlowing(request); + startFlowing(request, destination); destination.on('drain', createDrainHandler(destination, request)); }, abort() { diff --git a/packages/react-dom/src/server/ReactDOMLegacyServerBrowser.js b/packages/react-dom/src/server/ReactDOMLegacyServerBrowser.js index d05b44200e627..8982d4d9341bd 100644 --- a/packages/react-dom/src/server/ReactDOMLegacyServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMLegacyServerBrowser.js @@ -59,7 +59,6 @@ function renderToStringImpl( } const request = createRequest( children, - destination, createResponseState( generateStaticMarkup, options ? options.identifierPrefix : undefined, @@ -74,7 +73,7 @@ function renderToStringImpl( // If anything suspended and is still pending, we'll abort it before writing. // That way we write only client-rendered boundaries from the start. abort(request); - startFlowing(request); + startFlowing(request, destination); if (didFatal) { throw fatalError; } diff --git a/packages/react-dom/src/server/ReactDOMLegacyServerNode.js b/packages/react-dom/src/server/ReactDOMLegacyServerNode.js index ce2924c9a53a4..71ebab5ac0f0f 100644 --- a/packages/react-dom/src/server/ReactDOMLegacyServerNode.js +++ b/packages/react-dom/src/server/ReactDOMLegacyServerNode.js @@ -54,7 +54,7 @@ class ReactMarkupReadableStream extends Readable { _read(size) { if (this.startedFlowing) { - startFlowing(this.request); + startFlowing(this.request, this); } } } @@ -72,12 +72,11 @@ function renderToNodeStreamImpl( // We wait until everything has loaded before starting to write. // That way we only end up with fully resolved HTML even if we suspend. destination.startedFlowing = true; - startFlowing(request); + startFlowing(request, destination); } const destination = new ReactMarkupReadableStream(); const request = createRequest( children, - destination, createResponseState(false, options ? options.identifierPrefix : undefined), createRootFormatContext(), Infinity, diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index eed5f2219fbfd..f6d64c003f9e1 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -63,12 +63,11 @@ function render(model: ReactModel, options?: Options): Destination { const bundlerConfig = undefined; const request = ReactNoopFlightServer.createRequest( model, - destination, bundlerConfig, options ? options.onError : undefined, ); ReactNoopFlightServer.startWork(request); - ReactNoopFlightServer.startFlowing(request); + ReactNoopFlightServer.startFlowing(request, destination); return destination; } diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index 279c8b7b217fa..080c3042080a2 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -259,7 +259,6 @@ function render(children: React$Element, options?: Options): Destination { }; const request = ReactNoopServer.createRequest( children, - destination, null, null, options ? options.progressiveChunkSize : undefined, @@ -268,7 +267,7 @@ function render(children: React$Element, options?: Options): Destination { options ? options.onCompleteShell : undefined, ); ReactNoopServer.startWork(request); - ReactNoopServer.startFlowing(request); + ReactNoopServer.startFlowing(request, destination); return destination; } diff --git a/packages/react-server-dom-relay/src/ReactDOMServerFB.js b/packages/react-server-dom-relay/src/ReactDOMServerFB.js index 290a1f227e612..b5b6ba84e77d4 100644 --- a/packages/react-server-dom-relay/src/ReactDOMServerFB.js +++ b/packages/react-server-dom-relay/src/ReactDOMServerFB.js @@ -46,7 +46,6 @@ function renderToStream(children: ReactNodeList, options: Options): Stream { }; const request = createRequest( children, - destination, createResponseState(options ? options.identifierPrefix : undefined), createRootFormatContext(undefined), options ? options.progressiveChunkSize : undefined, @@ -71,7 +70,7 @@ function abortStream(stream: Stream): void { function renderNextChunk(stream: Stream): string { const {request, destination} = stream; performWork(request); - startFlowing(request); + startFlowing(request, destination); if (destination.fatal) { throw destination.error; } diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServer.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServer.js index fe2f9c8008c85..7feb6e3f0eff2 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServer.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServer.js @@ -31,12 +31,11 @@ function render( ): void { const request = createRequest( model, - destination, config, options ? options.onError : undefined, ); startWork(request); - startFlowing(request); + startFlowing(request, destination); } export {render}; diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js index 261cf85c9bdf4..9d632dfb0712e 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js @@ -25,15 +25,13 @@ function renderToReadableStream( webpackMap: BundlerConfig, options?: Options, ): ReadableStream { - let request; + const request = createRequest( + model, + webpackMap, + options ? options.onError : undefined, + ); const stream = new ReadableStream({ start(controller) { - request = createRequest( - model, - controller, - webpackMap, - options ? options.onError : undefined, - ); startWork(request); }, pull(controller) { @@ -42,7 +40,7 @@ function renderToReadableStream( // is actually used by something so we can give it the best result possible // at that point. if (stream.locked) { - startFlowing(request); + startFlowing(request, controller); } }, cancel(reason) {}, diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js index 4529abd2b6718..35773279890ca 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js @@ -18,7 +18,7 @@ import { } from 'react-server/src/ReactFlightServer'; function createDrainHandler(destination, request) { - return () => startFlowing(request); + return () => startFlowing(request, destination); } type Options = { @@ -33,12 +33,11 @@ function pipeToNodeWritable( ): void { const request = createRequest( model, - destination, webpackMap, options ? options.onError : undefined, ); startWork(request); - startFlowing(request); + startFlowing(request, destination); destination.on('drain', createDrainHandler(destination, request)); } diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayServer.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayServer.js index cae476b46c107..dd9bd6cc60ada 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayServer.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayServer.js @@ -24,9 +24,9 @@ function render( destination: Destination, config: BundlerConfig, ): void { - const request = createRequest(model, destination, config); + const request = createRequest(model, config); startWork(request); - startFlowing(request); + startFlowing(request, destination); } export {render}; diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index c50ef5335297c..e9396c861ee4c 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -166,15 +166,16 @@ type Segment = { +boundary: null | SuspenseBoundary, }; -const BUFFERING = 0; -const FLOWING = 1; +const OPEN = 0; +const CLOSING = 1; const CLOSED = 2; export opaque type Request = { - +destination: Destination, + destination: null | Destination, +responseState: ResponseState, +progressiveChunkSize: number, status: 0 | 1 | 2, + fatalError: mixed, nextSegmentId: number, allPendingTasks: number, // when it reaches zero, we can close the connection. pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary. @@ -221,7 +222,6 @@ function noop(): void {} export function createRequest( children: ReactNodeList, - destination: Destination, responseState: ResponseState, rootFormatContext: FormatContext, progressiveChunkSize: void | number, @@ -232,13 +232,14 @@ export function createRequest( const pingedTasks = []; const abortSet: Set = new Set(); const request = { - destination, + destination: null, responseState, progressiveChunkSize: progressiveChunkSize === undefined ? DEFAULT_PROGRESSIVE_CHUNK_SIZE : progressiveChunkSize, - status: BUFFERING, + status: OPEN, + fatalError: null, nextSegmentId: 0, allPendingTasks: 0, pendingRootTasks: 0, @@ -404,8 +405,13 @@ function fatalError(request: Request, error: mixed): void { // This is called outside error handling code such as if the root errors outside // a suspense boundary or if the root suspense boundary's fallback errors. // It's also called if React itself or its host configs errors. - request.status = CLOSED; - closeWithError(request.destination, error); + if (request.destination !== null) { + request.status = CLOSED; + closeWithError(request.destination, error); + } else { + request.status = CLOSING; + request.fatalError = error; + } } function renderSuspenseBoundary( @@ -1330,7 +1336,9 @@ function abortTask(task: Task): void { // the request; if (request.status !== CLOSED) { request.status = CLOSED; - close(request.destination); + if (request.destination !== null) { + close(request.destination); + } } } else { boundary.pendingTasks--; @@ -1490,8 +1498,8 @@ export function performWork(request: Request): void { retryTask(request, task); } pingedTasks.splice(0, i); - if (request.status === FLOWING) { - flushCompletedQueues(request); + if (request.destination !== null) { + flushCompletedQueues(request, request.destination); } } catch (error) { reportError(request, error); @@ -1748,8 +1756,10 @@ function flushPartiallyCompletedSegment( } } -function flushCompletedQueues(request: Request): void { - const destination = request.destination; +function flushCompletedQueues( + request: Request, + destination: Destination, +): void { beginWriting(destination); try { // The structure of this is to go through each queue one by one and write @@ -1775,7 +1785,7 @@ function flushCompletedQueues(request: Request): void { for (i = 0; i < clientRenderedBoundaries.length; i++) { const boundary = clientRenderedBoundaries[i]; if (!flushClientRenderedBoundary(request, destination, boundary)) { - request.status = BUFFERING; + request.destination = null; i++; clientRenderedBoundaries.splice(0, i); return; @@ -1790,7 +1800,7 @@ function flushCompletedQueues(request: Request): void { for (i = 0; i < completedBoundaries.length; i++) { const boundary = completedBoundaries[i]; if (!flushCompletedBoundary(request, destination, boundary)) { - request.status = BUFFERING; + request.destination = null; i++; completedBoundaries.splice(0, i); return; @@ -1811,7 +1821,7 @@ function flushCompletedQueues(request: Request): void { for (i = 0; i < partialBoundaries.length; i++) { const boundary = partialBoundaries[i]; if (!flushPartialBoundary(request, destination, boundary)) { - request.status = BUFFERING; + request.destination = null; i++; partialBoundaries.splice(0, i); return; @@ -1826,7 +1836,7 @@ function flushCompletedQueues(request: Request): void { for (i = 0; i < largeBoundaries.length; i++) { const boundary = largeBoundaries[i]; if (!flushCompletedBoundary(request, destination, boundary)) { - request.status = BUFFERING; + request.destination = null; i++; largeBoundaries.splice(0, i); return; @@ -1861,13 +1871,18 @@ export function startWork(request: Request): void { scheduleWork(() => performWork(request)); } -export function startFlowing(request: Request): void { +export function startFlowing(request: Request, destination: Destination): void { + if (request.status === CLOSING) { + request.status = CLOSED; + closeWithError(destination, request.fatalError); + return; + } if (request.status === CLOSED) { return; } - request.status = FLOWING; + request.destination = destination; try { - flushCompletedQueues(request); + flushCompletedQueues(request, destination); } catch (error) { reportError(request, error); fatalError(request, error); @@ -1880,8 +1895,8 @@ export function abort(request: Request): void { const abortableTasks = request.abortableTasks; abortableTasks.forEach(abortTask, request); abortableTasks.clear(); - if (request.status === FLOWING) { - flushCompletedQueues(request); + if (request.destination !== null) { + flushCompletedQueues(request, request.destination); } } catch (error) { reportError(request, error); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index f608c9d288a49..8247a904ec997 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -72,7 +72,9 @@ type Segment = { }; export type Request = { - destination: Destination, + status: 0 | 1 | 2, + fatalError: mixed, + destination: null | Destination, bundlerConfig: BundlerConfig, cache: Map, nextChunkId: number, @@ -84,25 +86,30 @@ export type Request = { writtenSymbols: Map, writtenModules: Map, onError: (error: mixed) => void, - flowing: boolean, toJSON: (key: string, value: ReactModel) => ReactJSONValue, }; const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; function defaultErrorHandler(error: mixed) { - console['error'](error); // Don't transform to our wrapper + console['error'](error); + // Don't transform to our wrapper } +const OPEN = 0; +const CLOSING = 1; +const CLOSED = 2; + export function createRequest( model: ReactModel, - destination: Destination, bundlerConfig: BundlerConfig, onError: void | ((error: mixed) => void), ): Request { const pingedSegments = []; const request = { - destination, + status: OPEN, + fatalError: null, + destination: null, bundlerConfig, cache: new Map(), nextChunkId: 0, @@ -114,7 +121,6 @@ export function createRequest( writtenSymbols: new Map(), writtenModules: new Map(), onError: onError === undefined ? defaultErrorHandler : onError, - flowing: false, toJSON: function(key: string, value: ReactModel): ReactJSONValue { return resolveModelToJSON(request, this, key, value); }, @@ -604,7 +610,13 @@ function reportError(request: Request, error: mixed): void { function fatalError(request: Request, error: mixed): void { // This is called outside error handling code such as if an error happens in React internals. - closeWithError(request.destination, error); + if (request.destination !== null) { + request.status = CLOSED; + closeWithError(request.destination, error); + } else { + request.status = CLOSING; + request.fatalError = error; + } } function emitErrorChunk(request: Request, id: number, error: mixed): void { @@ -694,8 +706,8 @@ function performWork(request: Request): void { const segment = pingedSegments[i]; retrySegment(request, segment); } - if (request.flowing) { - flushCompletedChunks(request); + if (request.destination !== null) { + flushCompletedChunks(request, request.destination); } } catch (error) { reportError(request, error); @@ -706,8 +718,10 @@ function performWork(request: Request): void { } } -function flushCompletedChunks(request: Request): void { - const destination = request.destination; +function flushCompletedChunks( + request: Request, + destination: Destination, +): void { beginWriting(destination); try { // We emit module chunks first in the stream so that @@ -718,7 +732,7 @@ function flushCompletedChunks(request: Request): void { request.pendingChunks--; const chunk = moduleChunks[i]; if (!writeChunk(destination, chunk)) { - request.flowing = false; + request.destination = null; i++; break; } @@ -731,7 +745,7 @@ function flushCompletedChunks(request: Request): void { request.pendingChunks--; const chunk = jsonChunks[i]; if (!writeChunk(destination, chunk)) { - request.flowing = false; + request.destination = null; i++; break; } @@ -746,7 +760,7 @@ function flushCompletedChunks(request: Request): void { request.pendingChunks--; const chunk = errorChunks[i]; if (!writeChunk(destination, chunk)) { - request.flowing = false; + request.destination = null; i++; break; } @@ -766,10 +780,18 @@ export function startWork(request: Request): void { scheduleWork(() => performWork(request)); } -export function startFlowing(request: Request): void { - request.flowing = true; +export function startFlowing(request: Request, destination: Destination): void { + if (request.status === CLOSING) { + request.status = CLOSED; + closeWithError(destination, request.fatalError); + return; + } + if (request.status === CLOSED) { + return; + } + request.destination = destination; try { - flushCompletedChunks(request); + flushCompletedChunks(request, destination); } catch (error) { reportError(request, error); fatalError(request, error); From 033efe7312cdf73118922b279d9b1ae29a2f693d Mon Sep 17 00:00:00 2001 From: salazarm Date: Tue, 28 Sep 2021 21:32:57 -0400 Subject: [PATCH 003/109] Call get snapshot in useSyncExternalStore server shim (#22453) * Call getSnapshot in shim * just change useSyncExternalStoreServer * remove builtInAPI Check in useSyncExternalStoreClient --- .../useSyncExternalStoreShimServer-test.js | 6 ++++-- .../src/useSyncExternalStore.js | 10 +++++++++- .../src/useSyncExternalStoreClient.js | 19 ++----------------- .../src/useSyncExternalStoreServer.js | 11 +---------- 4 files changed, 16 insertions(+), 30 deletions(-) diff --git a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShimServer-test.js b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShimServer-test.js index 9fa5b9ce21ebf..b46e028724061 100644 --- a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShimServer-test.js +++ b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShimServer-test.js @@ -92,7 +92,9 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => { } const html = ReactDOMServer.renderToString(); - expect(Scheduler).toHaveYielded(['server']); - expect(html).toEqual('server'); + + // We don't call getServerSnapshot in the shim + expect(Scheduler).toHaveYielded(['client']); + expect(html).toEqual('client'); }); }); diff --git a/packages/use-sync-external-store/src/useSyncExternalStore.js b/packages/use-sync-external-store/src/useSyncExternalStore.js index 8a1a5c7191135..c152287174269 100644 --- a/packages/use-sync-external-store/src/useSyncExternalStore.js +++ b/packages/use-sync-external-store/src/useSyncExternalStore.js @@ -10,5 +10,13 @@ import {canUseDOM} from 'shared/ExecutionEnvironment'; import {useSyncExternalStore as client} from './useSyncExternalStoreClient'; import {useSyncExternalStore as server} from './useSyncExternalStoreServer'; +import * as React from 'react'; -export const useSyncExternalStore = canUseDOM ? client : server; +const {unstable_useSyncExternalStore: builtInAPI} = React; + +export const useSyncExternalStore = + builtInAPI !== undefined + ? ((builtInAPI: any): typeof client) + : canUseDOM + ? client + : server; diff --git a/packages/use-sync-external-store/src/useSyncExternalStoreClient.js b/packages/use-sync-external-store/src/useSyncExternalStoreClient.js index 76e7fda36f831..dc42169c399d6 100644 --- a/packages/use-sync-external-store/src/useSyncExternalStoreClient.js +++ b/packages/use-sync-external-store/src/useSyncExternalStoreClient.js @@ -12,22 +12,7 @@ import is from 'shared/objectIs'; // Intentionally not using named imports because Rollup uses dynamic // dispatch for CommonJS interop named imports. -const { - useState, - useEffect, - useLayoutEffect, - useDebugValue, - // The built-in API is still prefixed. - unstable_useSyncExternalStore: builtInAPI, -} = React; - -// Prefer the built-in API, if it exists. If it doesn't exist, then we assume -// we're in version 16 or 17, so rendering is always synchronous. The shim -// does not support concurrent rendering, only the built-in API. -export const useSyncExternalStore = - builtInAPI !== undefined - ? ((builtInAPI: any): typeof useSyncExternalStore_client) - : useSyncExternalStore_client; +const {useState, useEffect, useLayoutEffect, useDebugValue} = React; let didWarnOld18Alpha = false; let didWarnUncachedGetSnapshot = false; @@ -42,7 +27,7 @@ let didWarnUncachedGetSnapshot = false; // // Do not assume that the clever hacks used by this hook also work in general. // The point of this shim is to replace the need for hacks by other libraries. -function useSyncExternalStore_client( +export function useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, // Note: The client shim does not use getServerSnapshot, because pre-18 diff --git a/packages/use-sync-external-store/src/useSyncExternalStoreServer.js b/packages/use-sync-external-store/src/useSyncExternalStoreServer.js index 1bf2a752273db..52903dd4aca89 100644 --- a/packages/use-sync-external-store/src/useSyncExternalStoreServer.js +++ b/packages/use-sync-external-store/src/useSyncExternalStoreServer.js @@ -7,19 +7,10 @@ * @flow */ -import invariant from 'shared/invariant'; - export function useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, getServerSnapshot?: () => T, ): T { - if (getServerSnapshot === undefined) { - invariant( - false, - 'Missing getServerSnapshot, which is required for server-' + - 'rendered content.', - ); - } - return getServerSnapshot(); + return getSnapshot(); } From 0883c4cd3a0bba4cc3eb2a1921cf21b1d78ddea2 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Wed, 29 Sep 2021 12:06:13 -0400 Subject: [PATCH 004/109] React DevTools 4.18.0 -> 4.19.0 (#22461) --- packages/react-devtools-core/package.json | 2 +- .../chrome/manifest.json | 4 ++-- .../edge/manifest.json | 4 ++-- .../firefox/manifest.json | 2 +- packages/react-devtools-inline/package.json | 2 +- .../package.json | 2 +- packages/react-devtools/CHANGELOG.md | 21 +++++++++++++++++++ packages/react-devtools/package.json | 4 ++-- 8 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json index 4d1ebdd9db8a4..4c6d33dbd585e 100644 --- a/packages/react-devtools-core/package.json +++ b/packages/react-devtools-core/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-core", - "version": "4.18.0", + "version": "4.19.0", "description": "Use react-devtools outside of the browser", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index ba5dd667cdd28..e20186cc22c52 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Chrome Developer Tools.", - "version": "4.18.0", - "version_name": "4.18.0", + "version": "4.19.0", + "version_name": "4.19.0", "minimum_chrome_version": "60", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index 0fdb82088c952..ad8d5eade5d30 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.", - "version": "4.18.0", - "version_name": "4.18.0", + "version": "4.19.0", + "version_name": "4.19.0", "minimum_chrome_version": "60", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 7ef9d847b15f7..56b0ee91269ca 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", - "version": "4.18.0", + "version": "4.19.0", "applications": { "gecko": { "id": "@react-devtools", diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index e6e9807c39013..a3905de97f0b6 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-inline", - "version": "4.18.0", + "version": "4.19.0", "description": "Embed react-devtools within a website", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-scheduling-profiler/package.json b/packages/react-devtools-scheduling-profiler/package.json index 98a386ba1949c..4b10d75406d3b 100644 --- a/packages/react-devtools-scheduling-profiler/package.json +++ b/packages/react-devtools-scheduling-profiler/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "react-devtools-scheduling-profiler", - "version": "4.18.0", + "version": "4.19.0", "license": "MIT", "dependencies": { "@elg/speedscope": "1.9.0-a6f84db", diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index d858072a8d7b3..e4ca09b65faf7 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -2,6 +2,27 @@ +## 4.19.0 (September 29, 2021) + +#### Features +* Scheduling Profiler: Show Suspense resource .displayName ([bvaughn](https://github.com/bvaughn) in [#22451](https://github.com/facebook/react/pull/22451)) +* Scheduling Profiler marks should include thrown Errors ([bvaughn](https://github.com/bvaughn) in [#22419](https://github.com/facebook/react/pull/22419)) +* Don't patch console during first render in strict mode ([lunaruan](https://github.com/lunaruan) in [#22308](https://github.com/facebook/react/pull/22308)) +* Show which hook indicies changed when profiling for all builds ([bvaughn](https://github.com/bvaughn) in [#22365](https://github.com/facebook/react/pull/22365)) +* Display actual ReactDOM API name in root type ([eps1lon](https://github.com/eps1lon) in [#22363](https://github.com/facebook/react/pull/22363)) +* Add named hooks support to standalone and inline DevTools ([jstejada](https://github.com/jstejada) in [#22320](https://github.com/facebook/react/pull/22320) and [bvaughn](https://github.com/bvaughn) in [#22263](https://github.com/facebook/react/pull/22263)) +#### Bugfix +* DevTools encoding supports multibyte characters (e.g. "🟩") ([bvaughn](https://github.com/bvaughn) in [#22424](https://github.com/facebook/react/pull/22424)) +* Improve DEV errors if string coercion throws (Temporal.*, Symbol, etc.) ([justingrant](https://github.com/justingrant) in [#22064](https://github.com/facebook/react/pull/22064)) +* Fix memory leak caused by not storing alternate Fiber pointer ([bvaughn](https://github.com/bvaughn) in [#22346](https://github.com/facebook/react/pull/22346)) +* Fix call stack exceeded error in `utfDecodeString()` ([bvaughn](https://github.com/bvaughn) in [#22330](https://github.com/facebook/react/pull/22330)) +* Fix runtime error when inspecting an element times out ([jstejada](https://github.com/jstejada) in [#22329](https://github.com/facebook/react/pull/22329)) + +#### Performance +* DevTools: Lazily parse indexed map sections ([bvaughn](https://github.com/bvaughn) in [#22415](https://github.com/facebook/react/pull/22415)) +* DevTools: Hook names optimizations ([bvaughn](https://github.com/bvaughn) in [#22403](https://github.com/facebook/react/pull/22403)) +* Replaced `network.onRequestFinished()` caching with `network.getHAR()` ([bvaughn](https://github.com/bvaughn) in [#22285](https://github.com/facebook/react/pull/22285)) + ## 4.18.0 (September 1, 2021) #### Features diff --git a/packages/react-devtools/package.json b/packages/react-devtools/package.json index 12f93895780c9..6ff26538ca967 100644 --- a/packages/react-devtools/package.json +++ b/packages/react-devtools/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools", - "version": "4.18.0", + "version": "4.19.0", "description": "Use react-devtools outside of the browser", "license": "MIT", "repository": { @@ -27,7 +27,7 @@ "electron": "^11.1.0", "ip": "^1.1.4", "minimist": "^1.2.3", - "react-devtools-core": "4.18.0", + "react-devtools-core": "4.19.0", "update-notifier": "^2.1.0" } } From 95ecd4a2c360921f9db91bf8db5cad1aa5a34a5e Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 29 Sep 2021 17:46:34 -0400 Subject: [PATCH 005/109] [DevTools] Commands for internal builds all follow :fb convention (#22463) --- packages/react-devtools-extensions/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-devtools-extensions/package.json b/packages/react-devtools-extensions/package.json index a2642788b5f4c..7d1f018a6bb2b 100644 --- a/packages/react-devtools-extensions/package.json +++ b/packages/react-devtools-extensions/package.json @@ -6,12 +6,12 @@ "build": "cross-env NODE_ENV=production yarn run build:chrome && yarn run build:firefox && yarn run build:edge", "build:dev": "cross-env NODE_ENV=development yarn run build:chrome:dev && yarn run build:firefox:dev && yarn run build:edge:dev", "build:chrome": "cross-env NODE_ENV=production node ./chrome/build", - "build:chrome:crx": "cross-env NODE_ENV=production FEATURE_FLAG_TARGET=extension-fb node ./chrome/build --crx", + "build:chrome:fb": "cross-env NODE_ENV=production FEATURE_FLAG_TARGET=extension-fb node ./chrome/build --crx", "build:chrome:dev": "cross-env NODE_ENV=development node ./chrome/build", "build:firefox": "cross-env NODE_ENV=production node ./firefox/build", "build:firefox:dev": "cross-env NODE_ENV=development node ./firefox/build", "build:edge": "cross-env NODE_ENV=production node ./edge/build", - "build:edge:crx": "cross-env NODE_ENV=production node ./edge/build --crx", + "build:edge:fb": "cross-env NODE_ENV=production FEATURE_FLAG_TARGET=extension-fb node ./edge/build --crx", "build:edge:dev": "cross-env NODE_ENV=development node ./edge/build", "test:chrome": "node ./chrome/test", "test:firefox": "node ./firefox/test", From 201af81b0168cabea3cc07cd8201378a4fec4aaf Mon Sep 17 00:00:00 2001 From: Joseph Savona Date: Wed, 29 Sep 2021 15:49:52 -0700 Subject: [PATCH 006/109] Release pooled cache reference in complete/unwind (#22464) --- .../src/ReactFiberCacheComponent.new.js | 10 ++++++---- .../src/ReactFiberCacheComponent.old.js | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCacheComponent.new.js b/packages/react-reconciler/src/ReactFiberCacheComponent.new.js index 042a8b2efc098..0d284c683ed1f 100644 --- a/packages/react-reconciler/src/ReactFiberCacheComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberCacheComponent.new.js @@ -99,14 +99,16 @@ export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) { return; } // The `pooledCache` variable points to the cache that was used for new - // cache boundaries during this render, if any. Stash it on the root so that - // parallel transitions may share the same cache. We will clear this field - // once all the transitions that depend on it (which we track with - // `pooledCacheLanes`) have committed. + // cache boundaries during this render, if any. Move ownership of the + // cache to the root so that parallel transitions may share the same + // cache. We will clear this field once all the transitions that depend + // on it (which we track with `pooledCacheLanes`) have committed. root.pooledCache = pooledCache; if (pooledCache !== null) { root.pooledCacheLanes |= renderLanes; } + // set to null, conceptually we are moving ownership to the root + pooledCache = null; } export function restoreSpawnedCachePool( diff --git a/packages/react-reconciler/src/ReactFiberCacheComponent.old.js b/packages/react-reconciler/src/ReactFiberCacheComponent.old.js index 2bc64254d3092..dd450fff76b50 100644 --- a/packages/react-reconciler/src/ReactFiberCacheComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberCacheComponent.old.js @@ -99,14 +99,16 @@ export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) { return; } // The `pooledCache` variable points to the cache that was used for new - // cache boundaries during this render, if any. Stash it on the root so that - // parallel transitions may share the same cache. We will clear this field - // once all the transitions that depend on it (which we track with - // `pooledCacheLanes`) have committed. + // cache boundaries during this render, if any. Move ownership of the + // cache to the root so that parallel transitions may share the same + // cache. We will clear this field once all the transitions that depend + // on it (which we track with `pooledCacheLanes`) have committed. root.pooledCache = pooledCache; if (pooledCache !== null) { root.pooledCacheLanes |= renderLanes; } + // set to null, conceptually we are moving ownership to the root + pooledCache = null; } export function restoreSpawnedCachePool( From ec4ac97500e850a90f837668a261533e780f40f4 Mon Sep 17 00:00:00 2001 From: Ay-Ay-Ron <34754995+aaronamendez@users.noreply.github.com> Date: Thu, 30 Sep 2021 09:58:01 -0400 Subject: [PATCH 007/109] Fixed Link on Documentation (#22465) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a5a64e0dc707..54a095f7d0f4c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ You can use React as a `'); @@ -1700,6 +1711,7 @@ export function writeCompletedSegmentInstruction( responseState: ResponseState, contentSegmentID: number, ): boolean { + writeChunk(destination, responseState.startInlineScript); if (!responseState.sentCompleteSegmentFunction) { // The first time we write this, we'll need to include the full implementation. responseState.sentCompleteSegmentFunction = true; @@ -1718,11 +1730,9 @@ export function writeCompletedSegmentInstruction( } const completeBoundaryScript1Full = stringToPrecomputedChunk( - ''); @@ -1732,6 +1742,7 @@ export function writeCompletedBoundaryInstruction( boundaryID: SuspenseBoundaryID, contentSegmentID: number, ): boolean { + writeChunk(destination, responseState.startInlineScript); if (!responseState.sentCompleteBoundaryFunction) { // The first time we write this, we'll need to include the full implementation. responseState.sentCompleteBoundaryFunction = true; @@ -1756,9 +1767,9 @@ export function writeCompletedBoundaryInstruction( } const clientRenderScript1Full = stringToPrecomputedChunk( - ''); export function writeClientRenderBoundaryInstruction( @@ -1766,6 +1777,7 @@ export function writeClientRenderBoundaryInstruction( responseState: ResponseState, boundaryID: SuspenseBoundaryID, ): boolean { + writeChunk(destination, responseState.startInlineScript); if (!responseState.sentClientRenderFunction) { // The first time we write this, we'll need to include the full implementation. responseState.sentClientRenderFunction = true; diff --git a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js index 59fb19e7986c8..723c03dfdba12 100644 --- a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js @@ -29,6 +29,7 @@ export const isPrimaryRenderer = false; export type ResponseState = { // Keep this in sync with ReactDOMServerFormatConfig + startInlineScript: PrecomputedChunk, placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, @@ -46,9 +47,10 @@ export function createResponseState( generateStaticMarkup: boolean, identifierPrefix: string | void, ): ResponseState { - const responseState = createResponseStateImpl(identifierPrefix); + const responseState = createResponseStateImpl(identifierPrefix, undefined); return { // Keep this in sync with ReactDOMServerFormatConfig + startInlineScript: responseState.startInlineScript, placeholderPrefix: responseState.placeholderPrefix, segmentPrefix: responseState.segmentPrefix, boundaryPrefix: responseState.boundaryPrefix, From cdb8a1d19d0c0d43a72c3f0fe739b04da247c360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 19 Oct 2021 22:36:10 -0400 Subject: [PATCH 052/109] [Fizz] Add option to inject bootstrapping script tags after the shell is injected (#22594) * Add option to inject bootstrap scripts These are emitted right after the shell as flushed. * Update ssr fixtures to use bootstrapScripts instead of manual script tag * Add option to FB renderer too --- fixtures/ssr/server/render.js | 1 + fixtures/ssr/src/components/Chrome.js | 1 - fixtures/ssr/yarn.lock | 10 +- fixtures/ssr2/package.json | 4 +- fixtures/ssr2/server/render.js | 1 + fixtures/ssr2/src/Html.js | 1 - fixtures/ssr2/yarn.lock | 5277 +++++++++++++++++ .../src/__tests__/ReactDOMFizzServer-test.js | 31 +- .../ReactDOMFizzServerBrowser-test.js | 16 + .../__tests__/ReactDOMFizzServerNode-test.js | 18 + .../src/server/ReactDOMFizzServerBrowser.js | 6 + .../src/server/ReactDOMFizzServerNode.js | 6 + .../src/server/ReactDOMServerFormatConfig.js | 48 + .../ReactDOMServerLegacyFormatConfig.js | 3 + .../server/ReactNativeServerFormatConfig.js | 7 + .../src/ReactNoopServer.js | 7 + .../src/ReactDOMServerFB.js | 11 +- .../ReactDOMServerFB-test.internal.js | 15 + packages/react-server/src/ReactFizzServer.js | 2 + .../forks/ReactServerFormatConfig.custom.js | 1 + 20 files changed, 5445 insertions(+), 21 deletions(-) create mode 100644 fixtures/ssr2/yarn.lock diff --git a/fixtures/ssr/server/render.js b/fixtures/ssr/server/render.js index e0fc50f3f9538..9857a8a83dcfa 100644 --- a/fixtures/ssr/server/render.js +++ b/fixtures/ssr/server/render.js @@ -21,6 +21,7 @@ export default function render(url, res) { }); let didError = false; const {pipe, abort} = renderToPipeableStream(, { + bootstrapScripts: [assets['main.js']], onCompleteShell() { // If something errored before we started streaming, we set the error code appropriately. res.statusCode = didError ? 500 : 200; diff --git a/fixtures/ssr/src/components/Chrome.js b/fixtures/ssr/src/components/Chrome.js index c895663710560..23056dab92a4a 100644 --- a/fixtures/ssr/src/components/Chrome.js +++ b/fixtures/ssr/src/components/Chrome.js @@ -46,7 +46,6 @@ export default class Chrome extends Component { __html: `assetManifest = ${JSON.stringify(assets)};`, }} /> - "`, + ); + }); + // @gate experimental it('emits all HTML as one unit if we wait until the end to start', async () => { let hasLoaded = false; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index 7fa1c208dffc9..bd0ca112a272b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -82,6 +82,24 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate experimental + it('should emit bootstrap script src at the end', () => { + const {writable, output} = getTestWritable(); + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( +
hello world
, + { + bootstrapScriptContent: 'INIT();', + bootstrapScripts: ['init.js'], + bootstrapModules: ['init.mjs'], + }, + ); + pipe(writable); + jest.runAllTimers(); + expect(output.result).toMatchInlineSnapshot( + `"
hello world
"`, + ); + }); + // @gate experimental it('should start writing after pipe', () => { const {writable, output} = getTestWritable(); diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index 2865ef46b2574..907f0823cffe9 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -27,6 +27,9 @@ type Options = {| identifierPrefix?: string, namespaceURI?: string, nonce?: string, + bootstrapScriptContent?: string, + bootstrapScripts?: Array, + bootstrapModules?: Array, progressiveChunkSize?: number, signal?: AbortSignal, onCompleteShell?: () => void, @@ -43,6 +46,9 @@ function renderToReadableStream( createResponseState( options ? options.identifierPrefix : undefined, options ? options.nonce : undefined, + options ? options.bootstrapScriptContent : undefined, + options ? options.bootstrapScripts : undefined, + options ? options.bootstrapModules : undefined, ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index fe532a32c3b50..33a7083bb95eb 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -32,6 +32,9 @@ type Options = {| identifierPrefix?: string, namespaceURI?: string, nonce?: string, + bootstrapScriptContent?: string, + bootstrapScripts?: Array, + bootstrapModules?: Array, progressiveChunkSize?: number, onCompleteShell?: () => void, onCompleteAll?: () => void, @@ -51,6 +54,9 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) { createResponseState( options ? options.identifierPrefix : undefined, options ? options.nonce : undefined, + options ? options.bootstrapScriptContent : undefined, + options ? options.bootstrapScripts : undefined, + options ? options.bootstrapModules : undefined, ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index 67d90f8513452..1f5a7a65bed59 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -59,6 +59,7 @@ export const isPrimaryRenderer = true; // Per response, global state that is not contextual to the rendering subtree. export type ResponseState = { + bootstrapChunks: Array, startInlineScript: PrecomputedChunk, placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, @@ -73,11 +74,19 @@ export type ResponseState = { }; const startInlineScript = stringToPrecomputedChunk(''); + +const startScriptSrc = stringToPrecomputedChunk(''); // Allows us to keep track of what we've already written so we can refer back to it. export function createResponseState( identifierPrefix: string | void, nonce: string | void, + bootstrapScriptContent: string | void, + bootstrapScripts: Array | void, + bootstrapModules: Array | void, ): ResponseState { const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix; const inlineScriptWithNonce = @@ -86,7 +95,34 @@ export function createResponseState( : stringToPrecomputedChunk( '"`, + ); + }); + it('emits all HTML as one unit if we wait until the end to start', async () => { let hasLoaded = false; let resolve; diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 4ab2f06d518db..6692b92648643 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -36,6 +36,7 @@ import { closeWithError, } from './ReactServerStreamConfig'; import { + writeCompletedRoot, writePlaceholder, writeStartCompletedSuspenseBoundary, writeStartPendingSuspenseBoundary, @@ -1779,6 +1780,7 @@ function flushCompletedQueues( if (completedRootSegment !== null && request.pendingRootTasks === 0) { flushSegment(request, destination, completedRootSegment); request.completedRootSegment = null; + writeCompletedRoot(destination, request.responseState); } // We emit client rendering instructions for already emitted boundaries first. diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js index d816198c64180..8cfe59ce1628c 100644 --- a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js @@ -44,6 +44,7 @@ export const pushStartCompletedSuspenseBoundary = $$$hostConfig.pushStartCompletedSuspenseBoundary; export const pushEndCompletedSuspenseBoundary = $$$hostConfig.pushEndCompletedSuspenseBoundary; +export const writeCompletedRoot = $$$hostConfig.writeCompletedRoot; export const writePlaceholder = $$$hostConfig.writePlaceholder; export const writeStartCompletedSuspenseBoundary = $$$hostConfig.writeStartCompletedSuspenseBoundary; From 5ca4b0433205d01294b6306a3a90af1103e37dc9 Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 20 Oct 2021 09:50:26 -0400 Subject: [PATCH 053/109] Dev Tools: Relax constraint on passing extensionId for backend init (#22597) --- packages/react-devtools-extensions/src/backend.js | 8 +++++--- packages/react-devtools-extensions/src/contentScript.js | 5 +++++ .../react-devtools-extensions/src/injectGlobalHook.js | 4 +++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/react-devtools-extensions/src/backend.js b/packages/react-devtools-extensions/src/backend.js index aa8578a108646..5b6821bbba038 100644 --- a/packages/react-devtools-extensions/src/backend.js +++ b/packages/react-devtools-extensions/src/backend.js @@ -2,7 +2,7 @@ // Running module factories is intentionally delayed until we know the hook exists. // This is to avoid issues like: https://github.com/facebook/react-devtools/issues/1039 -/** @flow */ +// @flow strict-local 'use strict'; @@ -13,15 +13,16 @@ function welcome(event) { ) { return; } + const extensionId = event.data.extensionId; window.removeEventListener('message', welcome); - setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__); + setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__, extensionId); } window.addEventListener('message', welcome); -function setup(hook) { +function setup(hook, extensionId) { if (hook == null) { // DevTools didn't get injected into this page (maybe b'c of the contentType). return; @@ -55,6 +56,7 @@ function setup(hook) { { source: 'react-devtools-bridge', payload: {event, payload}, + extensionId, }, '*', transferable, diff --git a/packages/react-devtools-extensions/src/contentScript.js b/packages/react-devtools-extensions/src/contentScript.js index c914c6e7b3dfc..179959f7e01ec 100644 --- a/packages/react-devtools-extensions/src/contentScript.js +++ b/packages/react-devtools-extensions/src/contentScript.js @@ -2,6 +2,8 @@ 'use strict'; +import {CURRENT_EXTENSION_ID} from './constants'; + let backendDisconnected: boolean = false; let backendInitialized: boolean = false; @@ -10,6 +12,7 @@ function sayHelloToBackend() { { source: 'react-devtools-content-script', hello: true, + extensionId: CURRENT_EXTENSION_ID, }, '*', ); @@ -20,6 +23,7 @@ function handleMessageFromDevtools(message) { { source: 'react-devtools-content-script', payload: message, + extensionId: CURRENT_EXTENSION_ID, }, '*', ); @@ -49,6 +53,7 @@ function handleDisconnect() { type: 'event', event: 'shutdown', }, + extensionId: CURRENT_EXTENSION_ID, }, '*', ); diff --git a/packages/react-devtools-extensions/src/injectGlobalHook.js b/packages/react-devtools-extensions/src/injectGlobalHook.js index 02d5109e291eb..a5d96966c7e10 100644 --- a/packages/react-devtools-extensions/src/injectGlobalHook.js +++ b/packages/react-devtools-extensions/src/injectGlobalHook.js @@ -31,12 +31,14 @@ window.addEventListener('message', function onMessage({data, source}) { if (source !== window || !data) { return; } - if (data.extensionId !== CURRENT_EXTENSION_ID) { + if (data.extensionId != null && data.extensionId !== CURRENT_EXTENSION_ID) { if (__DEBUG__) { console.log( `[injectGlobalHook] Received message '${data.source}' from different extension instance. Skipping message.`, { currentExtension: EXTENSION_INSTALLATION_TYPE, + currentExtensionId: CURRENT_EXTENSION_ID, + providedExtensionId: data.extensionId, }, ); } From c213030b4966e935bb36c138e28761ea22dd837d Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 20 Oct 2021 16:00:13 -0400 Subject: [PATCH 054/109] React DevTools 4.20.1 -> 4.20.2 (#22605) --- packages/react-devtools-core/package.json | 2 +- packages/react-devtools-extensions/chrome/manifest.json | 4 ++-- packages/react-devtools-extensions/edge/manifest.json | 4 ++-- packages/react-devtools-extensions/firefox/manifest.json | 2 +- packages/react-devtools-inline/package.json | 2 +- packages/react-devtools-scheduling-profiler/package.json | 2 +- packages/react-devtools/CHANGELOG.md | 6 ++++++ packages/react-devtools/package.json | 4 ++-- 8 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json index 5f26b46c970fd..d57fc10ff97ec 100644 --- a/packages/react-devtools-core/package.json +++ b/packages/react-devtools-core/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-core", - "version": "4.20.1", + "version": "4.20.2", "description": "Use react-devtools outside of the browser", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index 21babe5e90691..ecafb54ca6d34 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Chrome Developer Tools.", - "version": "4.20.1", - "version_name": "4.20.1", + "version": "4.20.2", + "version_name": "4.20.2", "minimum_chrome_version": "60", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index 7dab50d1ba25f..6b813661e48d6 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.", - "version": "4.20.1", - "version_name": "4.20.1", + "version": "4.20.2", + "version_name": "4.20.2", "minimum_chrome_version": "60", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index d83f2f3f50a2a..16f4e7821e476 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", - "version": "4.20.1", + "version": "4.20.2", "applications": { "gecko": { "id": "@react-devtools", diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index be6e005a07ae8..7e013af63677b 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-inline", - "version": "4.20.1", + "version": "4.20.2", "description": "Embed react-devtools within a website", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-scheduling-profiler/package.json b/packages/react-devtools-scheduling-profiler/package.json index 6491132afa35b..84fdf9d470fa2 100644 --- a/packages/react-devtools-scheduling-profiler/package.json +++ b/packages/react-devtools-scheduling-profiler/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "react-devtools-scheduling-profiler", - "version": "4.20.1", + "version": "4.20.2", "license": "MIT", "dependencies": { "@elg/speedscope": "1.9.0-a6f84db", diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index 0e0aa89631937..8f916003d79ff 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -2,6 +2,12 @@ +## 4.20.2 (October 20, 2021) + +#### Bugfix +* Dev Tools: Relax constraint on passing extensionId for backend init ([@jstejada](https://github.com/jstejada) in [#22597](https://github.com/facebook/react/pull/22597)) +* DevTools: Fix passing extensionId in evaled postMessage calls ([@jstejada](https://github.com/jstejada) in [#22590](https://github.com/facebook/react/pull/22590)) + ## 4.20.1 (October 19, 2021) #### Bugfix diff --git a/packages/react-devtools/package.json b/packages/react-devtools/package.json index b22ef0bab4334..00240aa38200b 100644 --- a/packages/react-devtools/package.json +++ b/packages/react-devtools/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools", - "version": "4.20.1", + "version": "4.20.2", "description": "Use react-devtools outside of the browser", "license": "MIT", "repository": { @@ -27,7 +27,7 @@ "electron": "^11.1.0", "ip": "^1.1.4", "minimist": "^1.2.3", - "react-devtools-core": "4.20.1", + "react-devtools-core": "4.20.2", "update-notifier": "^2.1.0" } } From f6abf4b400812c3207ca16aecb84fa3ce352e166 Mon Sep 17 00:00:00 2001 From: Konstantin Popov Date: Thu, 21 Oct 2021 06:22:41 +0300 Subject: [PATCH 055/109] Fix typos (#22494) --- packages/react-devtools-shared/src/backend/legacy/renderer.js | 2 +- packages/react-dom/src/test-utils/ReactTestUtils.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index d799ce72a275c..c5684f222fa31 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -716,7 +716,7 @@ export function attach( // Any time an inspected element has an update, // we should update the selected $r value as wel. - // Do this before dehyration (cleanForBridge). + // Do this before dehydration (cleanForBridge). updateSelectedElement(id); inspectedElement.context = cleanForBridge( diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index 1aadc90b250a8..c83b072447ba7 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -178,7 +178,7 @@ function findAllInRenderedTree(inst, test) { } /** - * Finds all instance of components in the rendered tree that are DOM + * Finds all instances of components in the rendered tree that are DOM * components with the class name matching `className`. * @return {array} an array of all the matches. */ @@ -233,7 +233,7 @@ function findRenderedDOMComponentWithClass(root, className) { } /** - * Finds all instance of components in the rendered tree that are DOM + * Finds all instances of components in the rendered tree that are DOM * components with the tag name matching `tagName`. * @return {array} an array of all the matches. */ From 4ba20579daf119639567f69a0cb38128532754c6 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 21 Oct 2021 14:40:41 -0400 Subject: [PATCH 056/109] Scheduling Profiler: De-emphasize React internal frames (#22588) This commit adds code to all React bundles to explicitly register the beginning and ending of the module. This is done by creating Error objects (which capture the file name, line number, and column number) and passing them explicitly to a DevTools hook (when present). Next, as the Scheduling Profiler logs metadata to the User Timing API, it prints these module ranges along with other metadata (like Lane values and profiler version number). Lastly, the Scheduling Profiler UI compares stack frames to these ranges when drawing the flame graph and dims or de-emphasizes frames that fall within an internal module. The net effect of this is that user code (and 3rd party code) stands out clearly in the flame graph while React internal modules are dimmed. Internal module ranges are completely optional. Older profiling samples, or ones recorded without the React DevTools extension installed, will simply not dim the internal frames. --- .../src/checkForDuplicateInstallations.js | 8 +- .../src/constants.js | 10 ++- .../src/CanvasPage.js | 1 + .../src/content-views/FlamechartView.js | 40 ++++++++-- .../src/content-views/constants.js | 12 +++ .../utils/__tests__/__modules__/module-one.js | 16 ++++ .../utils/__tests__/__modules__/module-two.js | 18 +++++ .../utils/__tests__/moduleFilters-test.js | 79 +++++++++++++++++++ .../src/content-views/utils/moduleFilters.js | 69 ++++++++++++++++ .../__tests__/preprocessData-test.internal.js | 4 + .../src/import-worker/preprocessData.js | 59 ++++++++++++++ .../src/types.js | 12 +++ .../src/backend/types.js | 5 ++ .../react-devtools-shared/src/constants.js | 10 +++ packages/react-devtools-shared/src/hook.js | 41 ++++++++++ .../src/SchedulingProfiler.js | 17 ++++ .../shared/registerInternalModuleStart.js | 20 +++++ packages/shared/registerInternalModuleStop.js | 20 +++++ scripts/rollup/build.js | 3 +- scripts/rollup/bundles.js | 62 +++++++++++++++ scripts/rollup/wrappers.js | 52 +++++++++++- 21 files changed, 543 insertions(+), 15 deletions(-) create mode 100644 packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-one.js create mode 100644 packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-two.js create mode 100644 packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/moduleFilters-test.js create mode 100644 packages/react-devtools-scheduling-profiler/src/content-views/utils/moduleFilters.js create mode 100644 packages/shared/registerInternalModuleStart.js create mode 100644 packages/shared/registerInternalModuleStop.js diff --git a/packages/react-devtools-extensions/src/checkForDuplicateInstallations.js b/packages/react-devtools-extensions/src/checkForDuplicateInstallations.js index d86cac7709295..01db8edb34f74 100644 --- a/packages/react-devtools-extensions/src/checkForDuplicateInstallations.js +++ b/packages/react-devtools-extensions/src/checkForDuplicateInstallations.js @@ -9,13 +9,15 @@ declare var chrome: any; -import {__DEBUG__} from 'react-devtools-shared/src/constants'; +import { + INTERNAL_EXTENSION_ID, + LOCAL_EXTENSION_ID, + __DEBUG__, +} from 'react-devtools-shared/src/constants'; import {getBrowserName} from './utils'; import { EXTENSION_INSTALL_CHECK, EXTENSION_INSTALLATION_TYPE, - INTERNAL_EXTENSION_ID, - LOCAL_EXTENSION_ID, } from './constants'; const IS_CHROME = getBrowserName() === 'Chrome'; diff --git a/packages/react-devtools-extensions/src/constants.js b/packages/react-devtools-extensions/src/constants.js index 310bbaca11563..c17ad4d64acd8 100644 --- a/packages/react-devtools-extensions/src/constants.js +++ b/packages/react-devtools-extensions/src/constants.js @@ -7,6 +7,12 @@ * @flow strict-local */ +import { + CHROME_WEBSTORE_EXTENSION_ID, + INTERNAL_EXTENSION_ID, + LOCAL_EXTENSION_ID, +} from 'react-devtools-shared/src/constants'; + declare var chrome: any; export const CURRENT_EXTENSION_ID = chrome.runtime.id; @@ -15,10 +21,6 @@ export const EXTENSION_INSTALL_CHECK = 'extension-install-check'; export const SHOW_DUPLICATE_EXTENSION_WARNING = 'show-duplicate-extension-warning'; -export const CHROME_WEBSTORE_EXTENSION_ID = 'fmkadmapgofadopljbjfkapdkoienihi'; -export const INTERNAL_EXTENSION_ID = 'dnjnjgbfilfphmojnmhliehogmojhclc'; -export const LOCAL_EXTENSION_ID = 'ikiahnapldjmdmpkmfhjdjilojjhgcbf'; - export const EXTENSION_INSTALLATION_TYPE: | 'public' | 'internal' diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 48efcf8105e7c..ab5c828280c0a 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -374,6 +374,7 @@ function AutoSizedCanvas({ surface, defaultFrame, data.flamechart, + data.internalModuleSourceToRanges, data.duration, ); flamechartViewRef.current = flamechartView; diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js index 86f411a5b6e24..bbc8dcf13936b 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -11,6 +11,7 @@ import type { Flamechart, FlamechartStackFrame, FlamechartStackLayer, + InternalModuleSourceToRanges, } from '../types'; import type { Interaction, @@ -30,6 +31,7 @@ import { rectIntersectsRect, verticallyStackedLayout, } from '../view-base'; +import {isInternalModule} from './utils/moduleFilters'; import { durationToWidth, positioningScaleFactor, @@ -76,6 +78,8 @@ class FlamechartStackLayerView extends View { /** A set of `stackLayer`'s frames, for efficient lookup. */ _stackFrameSet: Set; + _internalModuleSourceToRanges: InternalModuleSourceToRanges; + _intrinsicSize: Size; _hoveredStackFrame: FlamechartStackFrame | null = null; @@ -85,11 +89,13 @@ class FlamechartStackLayerView extends View { surface: Surface, frame: Rect, stackLayer: FlamechartStackLayer, + internalModuleSourceToRanges: InternalModuleSourceToRanges, duration: number, ) { super(surface, frame); this._stackLayer = stackLayer; this._stackFrameSet = new Set(stackLayer); + this._internalModuleSourceToRanges = internalModuleSourceToRanges; this._intrinsicSize = { width: duration, height: FLAMECHART_FRAME_HEIGHT, @@ -160,9 +166,19 @@ class FlamechartStackLayerView extends View { } const showHoverHighlight = _hoveredStackFrame === _stackLayer[i]; - context.fillStyle = showHoverHighlight - ? hoverColorForStackFrame(stackFrame) - : defaultColorForStackFrame(stackFrame); + + let textFillStyle; + if (isInternalModule(this._internalModuleSourceToRanges, stackFrame)) { + context.fillStyle = showHoverHighlight + ? COLORS.INTERNAL_MODULE_FRAME_HOVER + : COLORS.INTERNAL_MODULE_FRAME; + textFillStyle = COLORS.INTERNAL_MODULE_FRAME_TEXT; + } else { + context.fillStyle = showHoverHighlight + ? hoverColorForStackFrame(stackFrame) + : defaultColorForStackFrame(stackFrame); + textFillStyle = COLORS.TEXT_COLOR; + } const drawableRect = intersectionOfRects(nodeRect, visibleArea); context.fillRect( @@ -172,7 +188,9 @@ class FlamechartStackLayerView extends View { drawableRect.size.height, ); - drawText(name, context, nodeRect, drawableRect); + drawText(name, context, nodeRect, drawableRect, { + fillStyle: textFillStyle, + }); } // Render bottom border. @@ -264,13 +282,22 @@ export class FlamechartView extends View { surface: Surface, frame: Rect, flamechart: Flamechart, + internalModuleSourceToRanges: InternalModuleSourceToRanges, duration: number, ) { super(surface, frame, layeredLayout); - this.setDataAndUpdateSubviews(flamechart, duration); + this.setDataAndUpdateSubviews( + flamechart, + internalModuleSourceToRanges, + duration, + ); } - setDataAndUpdateSubviews(flamechart: Flamechart, duration: number) { + setDataAndUpdateSubviews( + flamechart: Flamechart, + internalModuleSourceToRanges: InternalModuleSourceToRanges, + duration: number, + ) { const {surface, frame, _onHover, _hoveredStackFrame} = this; // Clear existing rows on data update @@ -285,6 +312,7 @@ export class FlamechartView extends View { surface, frame, stackLayer, + internalModuleSourceToRanges, duration, ); this._verticalStackView.addSubview(rowView); diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js index a715945f77e92..4d896d1967e96 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -45,6 +45,9 @@ export const MIN_INTERVAL_SIZE_PX = 70; // TODO Replace this with "export let" vars export let COLORS = { BACKGROUND: '', + INTERNAL_MODULE_FRAME: '', + INTERNAL_MODULE_FRAME_HOVER: '', + INTERNAL_MODULE_FRAME_TEXT: '', NATIVE_EVENT: '', NATIVE_EVENT_HOVER: '', NETWORK_PRIMARY: '', @@ -107,6 +110,15 @@ export function updateColorsToMatchTheme(element: Element): boolean { COLORS = { BACKGROUND: computedStyle.getPropertyValue('--color-background'), + INTERNAL_MODULE_FRAME: computedStyle.getPropertyValue( + '--color-scheduling-profiler-internal-module', + ), + INTERNAL_MODULE_FRAME_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-internal-module-hover', + ), + INTERNAL_MODULE_FRAME_TEXT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-internal-module-text', + ), NATIVE_EVENT: computedStyle.getPropertyValue( '--color-scheduling-profiler-native-event', ), diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-one.js b/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-one.js new file mode 100644 index 0000000000000..6c3ace6bfbc65 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-one.js @@ -0,0 +1,16 @@ +/** + * 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 + */ + +export const outerErrorA = new Error(); + +export const moduleStartError = new Error(); +export const innerError = new Error(); +export const moduleStopError = new Error(); + +export const outerErrorB = new Error(); diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-two.js b/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-two.js new file mode 100644 index 0000000000000..994f317900555 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-two.js @@ -0,0 +1,18 @@ +/** + * 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 + */ + +export const moduleAStartError = new Error(); +export const innerErrorA = new Error(); +export const moduleAStopError = new Error(); + +export const outerError = new Error(); + +export const moduleBStartError = new Error(); +export const innerErrorB = new Error(); +export const moduleBStopError = new Error(); diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/moduleFilters-test.js b/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/moduleFilters-test.js new file mode 100644 index 0000000000000..d84840371daa1 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/moduleFilters-test.js @@ -0,0 +1,79 @@ +/** + * 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 {isInternalModule} from '../moduleFilters'; + +describe('isInternalModule', () => { + let map; + + function createFlamechartStackFrame(scriptUrl, locationLine, locationColumn) { + return { + name: 'test', + timestamp: 0, + duration: 1, + scriptUrl, + locationLine, + locationColumn, + }; + } + + function createStackFrame(fileName, lineNumber, columnNumber) { + return { + columnNumber: columnNumber, + lineNumber: lineNumber, + fileName: fileName, + functionName: 'test', + source: ` at test (${fileName}:${lineNumber}:${columnNumber})`, + }; + } + + beforeEach(() => { + map = new Map(); + map.set('foo', [ + [createStackFrame('foo', 10, 0), createStackFrame('foo', 15, 100)], + ]); + map.set('bar', [ + [createStackFrame('bar', 10, 0), createStackFrame('bar', 15, 100)], + [createStackFrame('bar', 20, 0), createStackFrame('bar', 25, 100)], + ]); + }); + + it('should properly identify stack frames within the provided module ranges', () => { + expect( + isInternalModule(map, createFlamechartStackFrame('foo', 10, 0)), + ).toBe(true); + expect( + isInternalModule(map, createFlamechartStackFrame('foo', 12, 35)), + ).toBe(true); + expect( + isInternalModule(map, createFlamechartStackFrame('foo', 15, 100)), + ).toBe(true); + expect( + isInternalModule(map, createFlamechartStackFrame('bar', 12, 0)), + ).toBe(true); + expect( + isInternalModule(map, createFlamechartStackFrame('bar', 22, 125)), + ).toBe(true); + }); + + it('should properly identify stack frames outside of the provided module ranges', () => { + expect(isInternalModule(map, createFlamechartStackFrame('foo', 9, 0))).toBe( + false, + ); + expect( + isInternalModule(map, createFlamechartStackFrame('foo', 15, 101)), + ).toBe(false); + expect( + isInternalModule(map, createFlamechartStackFrame('bar', 17, 0)), + ).toBe(false); + expect( + isInternalModule(map, createFlamechartStackFrame('baz', 12, 0)), + ).toBe(false); + }); +}); diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/moduleFilters.js b/packages/react-devtools-scheduling-profiler/src/content-views/utils/moduleFilters.js new file mode 100644 index 0000000000000..7030e6124b603 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/utils/moduleFilters.js @@ -0,0 +1,69 @@ +/** + * 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 { + FlamechartStackFrame, + InternalModuleSourceToRanges, +} from '../../types'; + +import { + CHROME_WEBSTORE_EXTENSION_ID, + INTERNAL_EXTENSION_ID, + LOCAL_EXTENSION_ID, +} from 'react-devtools-shared/src/constants'; + +export function isInternalModule( + internalModuleSourceToRanges: InternalModuleSourceToRanges, + flamechartStackFrame: FlamechartStackFrame, +): boolean { + const {locationColumn, locationLine, scriptUrl} = flamechartStackFrame; + + if (scriptUrl == null || locationColumn == null || locationLine == null) { + // This could indicate a browser-internal API like performance.mark(). + return false; + } + + // Internal modules are only registered if DevTools was running when the profile was captured, + // but DevTools should also hide its own frames to avoid over-emphasizing them. + if ( + // Handle webpack-internal:// sources + scriptUrl.includes('/react-devtools') || + scriptUrl.includes('/react_devtools') || + // Filter out known extension IDs + scriptUrl.includes(CHROME_WEBSTORE_EXTENSION_ID) || + scriptUrl.includes(INTERNAL_EXTENSION_ID) || + scriptUrl.includes(LOCAL_EXTENSION_ID) + // Unfortunately this won't get everything, like relatively loaded chunks or Web Worker files. + ) { + return true; + } + + // Filter out React internal packages. + const ranges = internalModuleSourceToRanges.get(scriptUrl); + if (ranges != null) { + for (let i = 0; i < ranges.length; i++) { + const [startStackFrame, stopStackFrame] = ranges[i]; + + const isAfterStart = + locationLine > startStackFrame.lineNumber || + (locationLine === startStackFrame.lineNumber && + locationColumn >= startStackFrame.columnNumber); + const isBeforeStop = + locationLine < stopStackFrame.lineNumber || + (locationLine === stopStackFrame.lineNumber && + locationColumn <= stopStackFrame.columnNumber); + + if (isAfterStart && isBeforeStop) { + return true; + } + } + } + + return false; +} diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js index e458f8060f84b..f8690dc57447c 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js @@ -282,6 +282,7 @@ describe('preprocessData', () => { "componentMeasures": Array [], "duration": 0.005, "flamechart": Array [], + "internalModuleSourceToRanges": Map {}, "laneToLabelMap": Map { 0 => "Sync", 1 => "InputContinuousHydration", @@ -449,6 +450,7 @@ describe('preprocessData', () => { "componentMeasures": Array [], "duration": 0.011, "flamechart": Array [], + "internalModuleSourceToRanges": Map {}, "laneToLabelMap": Map { 0 => "Sync", 1 => "InputContinuousHydration", @@ -636,6 +638,7 @@ describe('preprocessData', () => { "componentMeasures": Array [], "duration": 0.013, "flamechart": Array [], + "internalModuleSourceToRanges": Map {}, "laneToLabelMap": Map { 0 => "Sync", 1 => "InputContinuousHydration", @@ -914,6 +917,7 @@ describe('preprocessData', () => { ], "duration": 0.031, "flamechart": Array [], + "internalModuleSourceToRanges": Map {}, "laneToLabelMap": Map { 0 => "Sync", 1 => "InputContinuousHydration", diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js index aff78142b81a6..6c41de1e3e279 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -13,6 +13,7 @@ import { } from '@elg/speedscope'; import type {TimelineEvent} from '@elg/speedscope'; import type { + ErrorStackFrame, BatchUID, Flamechart, Milliseconds, @@ -30,6 +31,7 @@ import type { import {REACT_TOTAL_NUM_LANES, SCHEDULING_PROFILER_VERSION} from '../constants'; import InvalidProfileError from './InvalidProfileError'; import {getBatchRange} from '../utils/getBatchRange'; +import ErrorStackParser from 'error-stack-parser'; type MeasureStackElement = {| type: ReactMeasureType, @@ -43,6 +45,8 @@ type ProcessorState = {| asyncProcessingPromises: Promise[], batchUID: BatchUID, currentReactComponentMeasure: ReactComponentMeasure | null, + internalModuleCurrentStackFrame: ErrorStackFrame | null, + internalModuleStackStringSet: Set, measureStack: MeasureStackElement[], nativeEventStack: NativeEvent[], nextRenderShouldGenerateNewBatchID: boolean, @@ -793,6 +797,49 @@ function processTimelineEvent( ); } // eslint-disable-line brace-style + // Internal module ranges + else if (name.startsWith('--react-internal-module-start-')) { + const stackFrameStart = name.substr(30); + + if (!state.internalModuleStackStringSet.has(stackFrameStart)) { + state.internalModuleStackStringSet.add(stackFrameStart); + + const parsedStackFrameStart = parseStackFrame(stackFrameStart); + + state.internalModuleCurrentStackFrame = parsedStackFrameStart; + } + } else if (name.startsWith('--react-internal-module-stop-')) { + const stackFrameStop = name.substr(19); + + if (!state.internalModuleStackStringSet.has(stackFrameStop)) { + state.internalModuleStackStringSet.add(stackFrameStop); + + const parsedStackFrameStop = parseStackFrame(stackFrameStop); + + if ( + parsedStackFrameStop !== null && + state.internalModuleCurrentStackFrame !== null + ) { + const parsedStackFrameStart = state.internalModuleCurrentStackFrame; + + state.internalModuleCurrentStackFrame = null; + + const range = [parsedStackFrameStart, parsedStackFrameStop]; + const ranges = currentProfilerData.internalModuleSourceToRanges.get( + parsedStackFrameStart.fileName, + ); + if (ranges == null) { + currentProfilerData.internalModuleSourceToRanges.set( + parsedStackFrameStart.fileName, + [range], + ); + } else { + ranges.push(range); + } + } + } + } // eslint-disable-line brace-style + // Other user timing marks/measures else if (ph === 'R' || ph === 'n') { // User Timing mark @@ -855,6 +902,15 @@ function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart { return flamechart; } +function parseStackFrame(stackFrame: string): ErrorStackFrame | null { + const error = new Error(); + error.stack = stackFrame; + + const frames = ErrorStackParser.parse(error); + + return frames.length === 1 ? frames[0] : null; +} + export default async function preprocessData( timeline: TimelineEvent[], ): Promise { @@ -870,6 +926,7 @@ export default async function preprocessData( componentMeasures: [], duration: 0, flamechart, + internalModuleSourceToRanges: new Map(), laneToLabelMap: new Map(), laneToReactMeasureMap, nativeEvents: [], @@ -913,6 +970,8 @@ export default async function preprocessData( asyncProcessingPromises: [], batchUID: 0, currentReactComponentMeasure: null, + internalModuleCurrentStackFrame: null, + internalModuleStackStringSet: new Set(), measureStack: [], nativeEventStack: [], nextRenderShouldGenerateNewBatchID: true, diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js index 4bfafe9a2eccc..e5b14e7897ace 100644 --- a/packages/react-devtools-scheduling-profiler/src/types.js +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -17,6 +17,12 @@ export type Return = Return_<*, T>; // Project types +export type ErrorStackFrame = { + fileName: string, + lineNumber: number, + columnNumber: number, +}; + export type Milliseconds = number; export type ReactLane = number; @@ -169,11 +175,17 @@ export type ViewState = {| viewToMutableViewStateMap: Map, |}; +export type InternalModuleSourceToRanges = Map< + string, + Array<[ErrorStackFrame, ErrorStackFrame]>, +>; + export type ReactProfilerData = {| batchUIDToMeasuresMap: Map, componentMeasures: ReactComponentMeasure[], duration: number, flamechart: Flamechart, + internalModuleSourceToRanges: InternalModuleSourceToRanges, laneToLabelMap: Map, laneToReactMeasureMap: Map, nativeEvents: NativeEvent[], diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 3f623ea656e48..46e1287797981 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -420,6 +420,11 @@ export type DevToolsHook = { didError?: boolean, ) => void, + // Scheduling Profiler internal module filtering + getInternalModuleRanges: () => Array<[string, string]>, + registerInternalModuleStart: (moduleStartError: Error) => void, + registerInternalModuleStop: (moduleStopError: Error) => void, + // Testing dangerous_setTargetConsoleForTesting?: (fakeConsole: Object) => void, ... diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 40c28a0f6abdf..3c3aaae2fc461 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -7,6 +7,10 @@ * @flow */ +export const CHROME_WEBSTORE_EXTENSION_ID = 'fmkadmapgofadopljbjfkapdkoienihi'; +export const INTERNAL_EXTENSION_ID = 'dnjnjgbfilfphmojnmhliehogmojhclc'; +export const LOCAL_EXTENSION_ID = 'ikiahnapldjmdmpkmfhjdjilojjhgcbf'; + // Flip this flag to true to enable verbose console debug logging. export const __DEBUG__ = false; @@ -147,6 +151,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-resize-bar-active': '#dcdcdc', '--color-resize-bar-border': '#d1d1d1', '--color-resize-bar-dot': '#333333', + '--color-scheduling-profiler-internal-module': '#d1d1d1', + '--color-scheduling-profiler-internal-module-hover': '#c9c9c9', + '--color-scheduling-profiler-internal-module-text': '#444', '--color-scheduling-profiler-native-event': '#ccc', '--color-scheduling-profiler-native-event-hover': '#aaa', '--color-scheduling-profiler-network-primary': '#fcf3dc', @@ -288,6 +295,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-resize-bar-active': '#31363f', '--color-resize-bar-border': '#3d424a', '--color-resize-bar-dot': '#cfd1d5', + '--color-scheduling-profiler-internal-module': '#303542', + '--color-scheduling-profiler-internal-module-hover': '#363b4a', + '--color-scheduling-profiler-internal-module-text': '#7f8899', '--color-scheduling-profiler-native-event': '#b2b2b2', '--color-scheduling-profiler-native-event-hover': '#949494', '--color-scheduling-profiler-network-primary': '#fcf3dc', diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index 6f34f86132ab6..e265212e31c42 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -490,6 +490,40 @@ export function installHook(target: any): DevToolsHook | null { } } + type StackFrameString = string; + + const openModuleRangesStack: Array = []; + const moduleRanges: Array<[StackFrameString, StackFrameString]> = []; + + function getTopStackFrameString(error: Error): StackFrameString | null { + const frames = error.stack.split('\n'); + const frame = frames.length > 1 ? frames[1] : null; + return frame; + } + + function getInternalModuleRanges(): Array< + [StackFrameString, StackFrameString], + > { + return moduleRanges; + } + + function registerInternalModuleStart(error: Error) { + const startStackFrame = getTopStackFrameString(error); + if (startStackFrame !== null) { + openModuleRangesStack.push(startStackFrame); + } + } + + function registerInternalModuleStop(error: Error) { + if (openModuleRangesStack.length > 0) { + const startStackFrame = openModuleRangesStack.pop(); + const stopStackFrame = getTopStackFrameString(error); + if (stopStackFrame !== null) { + moduleRanges.push([startStackFrame, stopStackFrame]); + } + } + } + // TODO: More meaningful names for "rendererInterfaces" and "renderers". const fiberRoots = {}; const rendererInterfaces = new Map(); @@ -520,6 +554,13 @@ export function installHook(target: any): DevToolsHook | null { onCommitFiberRoot, onPostCommitFiberRoot, setStrictMode, + + // Schedule Profiler runtime helpers. + // These internal React modules to report their own boundaries + // which in turn enables the profiler to dim or filter internal frames. + getInternalModuleRanges, + registerInternalModuleStart, + registerInternalModuleStop, }; if (__TEST__) { diff --git a/packages/react-reconciler/src/SchedulingProfiler.js b/packages/react-reconciler/src/SchedulingProfiler.js index 03148d499282d..4df55137fda97 100644 --- a/packages/react-reconciler/src/SchedulingProfiler.js +++ b/packages/react-reconciler/src/SchedulingProfiler.js @@ -98,6 +98,22 @@ function markVersionMetadata() { markAndClear(`--profiler-version-${SCHEDULING_PROFILER_VERSION}`); } +function markInternalModuleRanges() { + /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ + if ( + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.getInternalModuleRanges === 'function' + ) { + const ranges = __REACT_DEVTOOLS_GLOBAL_HOOK__.getInternalModuleRanges(); + for (let i = 0; i < ranges.length; i++) { + const [startStackFrame, stopStackFrame] = ranges[i]; + + markAndClear(`--react-internal-module-start-${startStackFrame}`); + markAndClear(`--react-internal-module-stop-${stopStackFrame}`); + } + } +} + export function markCommitStarted(lanes: Lanes): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { @@ -114,6 +130,7 @@ export function markCommitStarted(lanes: Lanes): void { // we can log this data only once (when started) and remove the per-commit logging. markVersionMetadata(); markLaneToLabelMetadata(); + markInternalModuleRanges(); } } } diff --git a/packages/shared/registerInternalModuleStart.js b/packages/shared/registerInternalModuleStart.js new file mode 100644 index 0000000000000..aa1154fe9f1bf --- /dev/null +++ b/packages/shared/registerInternalModuleStart.js @@ -0,0 +1,20 @@ +/** + * 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 + */ + +/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ + +// Don't require this file directly; it's embedded by Rollup during build. + +if ( + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart === + 'function' +) { + __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error()); +} diff --git a/packages/shared/registerInternalModuleStop.js b/packages/shared/registerInternalModuleStop.js new file mode 100644 index 0000000000000..dab139f2557f9 --- /dev/null +++ b/packages/shared/registerInternalModuleStop.js @@ -0,0 +1,20 @@ +/** + * 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 + */ + +/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ + +// Don't require this file directly; it's embedded by Rollup during build. + +if ( + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop === + 'function' +) { + __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error()); +} diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 4f7d26cddbec9..7e29c0e8c54d3 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -440,7 +440,8 @@ function getPlugins( bundleType, globalName, filename, - moduleType + moduleType, + bundle.wrapWithModuleBoundaries ); }, }, diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index c24281d43ef08..af7b30b7feb99 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -80,6 +80,7 @@ const bundles = [ entry: 'react', global: 'React', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: true, externals: ['ReactNativeInternalFeatureFlags'], }, @@ -90,6 +91,7 @@ const bundles = [ entry: 'react/unstable-shared-subset', global: 'React', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: [], }, @@ -108,6 +110,7 @@ const bundles = [ entry: 'react/jsx-runtime', global: 'JSXRuntime', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: ['react', 'ReactNativeInternalFeatureFlags'], }, @@ -128,6 +131,7 @@ const bundles = [ entry: 'react/jsx-dev-runtime', global: 'JSXDEVRuntime', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react', 'ReactNativeInternalFeatureFlags'], }, @@ -138,6 +142,7 @@ const bundles = [ entry: 'react-fetch/index.browser', global: 'ReactFetch', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -148,6 +153,7 @@ const bundles = [ entry: 'react-fetch/index.node', global: 'ReactFetch', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react', 'http', 'https'], }, @@ -158,6 +164,7 @@ const bundles = [ entry: 'react-fs/index.browser.server', global: 'ReactFilesystem', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: [], }, @@ -168,6 +175,7 @@ const bundles = [ entry: 'react-fs/index.node.server', global: 'ReactFilesystem', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react', 'fs/promises', 'path'], }, @@ -178,6 +186,7 @@ const bundles = [ entry: 'react-pg/index.browser.server', global: 'ReactPostgres', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: [], }, @@ -188,6 +197,7 @@ const bundles = [ entry: 'react-pg/index.node.server', global: 'ReactPostgres', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react', 'pg'], }, @@ -208,6 +218,7 @@ const bundles = [ entry: 'react-dom', global: 'ReactDOM', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: true, externals: ['react'], }, @@ -219,6 +230,7 @@ const bundles = [ global: 'ReactDOMForked', enableNewReconciler: true, minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: true, externals: ['react'], }, @@ -229,6 +241,7 @@ const bundles = [ entry: 'react-dom/test-utils', global: 'ReactTestUtils', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react', 'react-dom'], }, @@ -240,6 +253,7 @@ const bundles = [ entry: 'react-dom/testing', global: 'ReactDOMTesting', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -253,6 +267,7 @@ const bundles = [ name: 'react-dom-server-legacy.browser', global: 'ReactDOMServer', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: ['react'], babel: opts => Object.assign({}, opts, { @@ -268,6 +283,7 @@ const bundles = [ name: 'react-dom-server-legacy.node', externals: ['react', 'stream'], minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, babel: opts => Object.assign({}, opts, { plugins: opts.plugins.concat([ @@ -284,6 +300,7 @@ const bundles = [ name: 'react-dom-server.browser', global: 'ReactDOMServer', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: ['react'], }, { @@ -293,6 +310,7 @@ const bundles = [ name: 'react-dom-server.node', global: 'ReactDOMServer', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react'], }, { @@ -301,6 +319,7 @@ const bundles = [ entry: 'react-server-dom-relay/src/ReactDOMServerFB', global: 'ReactDOMServer', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -311,6 +330,7 @@ const bundles = [ entry: 'react-server-dom-webpack/writer.browser.server', global: 'ReactServerDOMWriter', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react'], }, { @@ -319,6 +339,7 @@ const bundles = [ entry: 'react-server-dom-webpack/writer.node.server', global: 'ReactServerDOMWriter', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -329,6 +350,7 @@ const bundles = [ entry: 'react-server-dom-webpack', global: 'ReactServerDOMReader', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -339,6 +361,7 @@ const bundles = [ entry: 'react-server-dom-webpack/plugin', global: 'ReactServerWebpackPlugin', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['fs', 'path', 'url', 'neo-async'], }, @@ -349,6 +372,7 @@ const bundles = [ entry: 'react-server-dom-webpack/node-loader', global: 'ReactServerWebpackNodeLoader', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['acorn'], }, @@ -359,6 +383,7 @@ const bundles = [ entry: 'react-server-dom-webpack/node-register', global: 'ReactFlightWebpackNodeRegister', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['url', 'module'], }, @@ -369,6 +394,7 @@ const bundles = [ entry: 'react-server-dom-relay/server', global: 'ReactFlightDOMRelayServer', // TODO: Rename to Writer minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: [ 'react', 'ReactFlightDOMRelayServerIntegration', @@ -383,6 +409,7 @@ const bundles = [ entry: 'react-server-dom-relay', global: 'ReactFlightDOMRelayClient', // TODO: Rename to Reader minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: [ 'react', 'ReactFlightDOMRelayClientIntegration', @@ -397,6 +424,7 @@ const bundles = [ entry: 'react-server-native-relay/server', global: 'ReactFlightNativeRelayServer', // TODO: Rename to Writer minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: [ 'react', 'ReactFlightNativeRelayServerIntegration', @@ -412,6 +440,7 @@ const bundles = [ entry: 'react-server-native-relay', global: 'ReactFlightNativeRelayClient', // TODO: Rename to Reader minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: [ 'react', 'ReactFlightNativeRelayClientIntegration', @@ -427,6 +456,7 @@ const bundles = [ entry: 'react-suspense-test-utils', global: 'ReactSuspenseTestUtils', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -445,6 +475,7 @@ const bundles = [ global: 'ReactART', externals: ['react'], minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: true, babel: opts => Object.assign({}, opts, { // Include JSX @@ -468,6 +499,7 @@ const bundles = [ global: 'ReactNativeRenderer', externals: ['react-native', 'ReactNativeInternalFeatureFlags'], minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: true, babel: opts => Object.assign({}, opts, { plugins: opts.plugins.concat([ @@ -482,6 +514,7 @@ const bundles = [ global: 'ReactNativeRenderer', externals: ['react-native'], minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: true, babel: opts => Object.assign({}, opts, { plugins: opts.plugins.concat([ @@ -500,6 +533,7 @@ const bundles = [ global: 'ReactFabric', externals: ['react-native', 'ReactNativeInternalFeatureFlags'], minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: true, babel: opts => Object.assign({}, opts, { plugins: opts.plugins.concat([ @@ -514,6 +548,7 @@ const bundles = [ global: 'ReactFabric', externals: ['react-native'], minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: true, babel: opts => Object.assign({}, opts, { plugins: opts.plugins.concat([ @@ -544,6 +579,7 @@ const bundles = [ 'ReactNativeInternalFeatureFlags', ], minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, babel: opts => Object.assign({}, opts, { plugins: opts.plugins.concat([ @@ -559,6 +595,7 @@ const bundles = [ entry: 'react-noop-renderer', global: 'ReactNoopRenderer', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: ['react', 'scheduler', 'scheduler/unstable_mock', 'expect'], }, @@ -569,6 +606,7 @@ const bundles = [ entry: 'react-noop-renderer/persistent', global: 'ReactNoopRendererPersistent', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: ['react', 'scheduler', 'expect'], }, @@ -579,6 +617,7 @@ const bundles = [ entry: 'react-noop-renderer/server', global: 'ReactNoopRendererServer', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: ['react', 'scheduler', 'expect'], }, @@ -589,6 +628,7 @@ const bundles = [ entry: 'react-noop-renderer/flight-server', global: 'ReactNoopFlightServer', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: [ 'react', 'scheduler', @@ -604,6 +644,7 @@ const bundles = [ entry: 'react-noop-renderer/flight-client', global: 'ReactNoopFlightClient', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: [ 'react', 'scheduler', @@ -619,6 +660,7 @@ const bundles = [ entry: 'react-reconciler', global: 'ReactReconciler', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -629,6 +671,7 @@ const bundles = [ entry: 'react-server', global: 'ReactServer', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -639,6 +682,7 @@ const bundles = [ entry: 'react-server/flight', global: 'ReactFlightServer', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -649,6 +693,7 @@ const bundles = [ entry: 'react-client/flight', global: 'ReactFlightClient', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -659,6 +704,7 @@ const bundles = [ entry: 'react-reconciler/reflection', global: 'ReactFiberTreeReflection', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: [], }, @@ -669,6 +715,7 @@ const bundles = [ entry: 'react-reconciler/constants', global: 'ReactReconcilerConstants', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: [], }, @@ -686,6 +733,7 @@ const bundles = [ entry: 'react-is', global: 'ReactIs', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: [], }, @@ -696,6 +744,7 @@ const bundles = [ entry: 'react-debug-tools', global: 'ReactDebugTools', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: [], }, @@ -708,6 +757,7 @@ const bundles = [ entry: 'react-cache', global: 'ReactCacheOld', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react', 'scheduler'], }, @@ -719,6 +769,7 @@ const bundles = [ global: 'createSubscription', externals: ['react'], minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: true, babel: opts => Object.assign({}, opts, { plugins: opts.plugins.concat([ @@ -734,6 +785,7 @@ const bundles = [ entry: 'use-subscription', global: 'useSubscription', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: true, externals: ['react'], }, @@ -744,6 +796,7 @@ const bundles = [ entry: 'use-sync-external-store', global: 'useSyncExternalStore', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: true, externals: ['react'], }, @@ -754,6 +807,7 @@ const bundles = [ entry: 'use-sync-external-store/extra', global: 'useSyncExternalStoreExtra', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: true, externals: ['react', 'use-sync-external-store'], }, @@ -764,6 +818,7 @@ const bundles = [ entry: 'use-sync-external-store/index.native', global: 'useSyncExternalStoreNative', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: true, externals: ['react', 'ReactNativeInternalFeatureFlags'], }, @@ -783,6 +838,7 @@ const bundles = [ entry: 'scheduler', global: 'Scheduler', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: true, externals: ['ReactNativeInternalFeatureFlags'], }, @@ -802,6 +858,7 @@ const bundles = [ entry: 'scheduler/unstable_mock', global: 'SchedulerMock', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['ReactNativeInternalFeatureFlags'], }, @@ -818,6 +875,7 @@ const bundles = [ entry: 'scheduler/unstable_post_task', global: 'SchedulerPostTask', minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, externals: [], }, @@ -828,6 +886,7 @@ const bundles = [ entry: 'jest-react', global: 'JestReact', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: ['react', 'scheduler', 'scheduler/unstable_mock'], }, @@ -842,6 +901,7 @@ const bundles = [ entry: 'eslint-plugin-react-hooks', global: 'ESLintPluginReactHooks', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: [], }, @@ -852,6 +912,7 @@ const bundles = [ entry: 'react-refresh/babel', global: 'ReactFreshBabelPlugin', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: [], }, { @@ -860,6 +921,7 @@ const bundles = [ entry: 'react-refresh/runtime', global: 'ReactFreshRuntime', minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, externals: [], }, ]; diff --git a/scripts/rollup/wrappers.js b/scripts/rollup/wrappers.js index 836929dbec87f..c83ecc0fbdb96 100644 --- a/scripts/rollup/wrappers.js +++ b/scripts/rollup/wrappers.js @@ -1,5 +1,7 @@ 'use strict'; +const {resolve} = require('path'); +const {readFileSync} = require('fs'); const {bundleTypes, moduleTypes} = require('./bundles'); const reactVersion = require('../../package.json').version; @@ -25,6 +27,26 @@ const { const {RECONCILER} = moduleTypes; +function registerInternalModuleStart(globalName) { + const path = resolve( + __dirname, + '..', + '..', + 'packages/shared/registerInternalModuleStart.js' + ); + return String(readFileSync(path)).trim(); +} + +function registerInternalModuleStop(globalName) { + const path = resolve( + __dirname, + '..', + '..', + 'packages/shared/registerInternalModuleStop.js' + ); + return String(readFileSync(path)).trim(); +} + const license = ` * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the @@ -319,7 +341,35 @@ ${source} }, }; -function wrapBundle(source, bundleType, globalName, filename, moduleType) { +function wrapBundle( + source, + bundleType, + globalName, + filename, + moduleType, + wrapWithModuleBoundaries +) { + if (wrapWithModuleBoundaries) { + switch (bundleType) { + case NODE_DEV: + case NODE_PROFILING: + case FB_WWW_DEV: + case FB_WWW_PROFILING: + case RN_OSS_DEV: + case RN_OSS_PROFILING: + case RN_FB_DEV: + case RN_FB_PROFILING: + // Certain DEV and Profiling bundles should self-register their own module boundaries with DevTools. + // This allows the Scheduling Profiler to de-emphasize (dim) internal stack frames. + source = ` + ${registerInternalModuleStart(globalName)} + ${source} + ${registerInternalModuleStop(globalName)} + `; + break; + } + } + if (moduleType === RECONCILER) { // Standalone reconciler is only used by third-party renderers. // It is handled separately. From 0e8a5aff3ddf0f863839a924738f958fd940e3be Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 21 Oct 2021 14:41:44 -0400 Subject: [PATCH 057/109] Scheduling Profiler: Add marks for component effects (mount and unmount) (#22578) DevTools (and its profilers) should not require users to be familiar with React internals. Although the scheduling profiler includes a CPU sample flame graph, it's there for advanced use cases and shouldn't be required to identify common performance issues. This PR proposes adding new marks around component effects. This will enable users to identify components with slow effect create/destroy functions without requiring them to dig through the call stack. (Once #22529 lands, these new marks will also include component stacks, making them more useful still.) For example, here's a profile with a long-running effect. Without this change, it's not clear why the effects phase takes so long. After this change, it's more clear why that the phase is longer because of a specific component. We may consider adding similar marks around render phase hooks like useState, useReducer, useMemo. I avoided doing that in this PR because it would be a pretty pervasive change to the ReactFiberHooks file. Note that this change should have no effect on production bundles since everything is guarded behind a profiling feature flag. Going to tag more people than I normally would for this pR, since it touches both reconciler and DevTools packages. Feel free to ignore though if you don't have strong feelings. Note that although this PR adds new marks to the scheduling profiler, it's done in a way that's backwards compatible for older profiles. --- .../src/EventTooltip.js | 21 +- .../content-views/ComponentMeasuresView.js | 57 +++- .../src/content-views/constants.js | 8 - .../__tests__/preprocessData-test.internal.js | 62 +++-- .../src/import-worker/preprocessData.js | 181 +++++++++++-- .../src/types.js | 8 + .../src/ReactFiberCommitWork.new.js | 62 ++++- .../src/ReactFiberCommitWork.old.js | 62 ++++- .../src/SchedulingProfiler.js | 72 ++++++ .../SchedulingProfiler-test.internal.js | 243 ++++++++++++++---- 10 files changed, 649 insertions(+), 127 deletions(-) diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js index 9152c7de8f56e..d3ca5d5625f13 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js @@ -140,9 +140,26 @@ const TooltipReactComponentMeasure = ({ }: {| componentMeasure: ReactComponentMeasure, |}) => { - const {componentName, duration, timestamp, warning} = componentMeasure; + const {componentName, duration, timestamp, type, warning} = componentMeasure; - const label = `${componentName} rendered`; + let label = componentName; + switch (type) { + case 'render': + label += ' rendered'; + break; + case 'layout-effect-mount': + label += ' mounted layout effect'; + break; + case 'layout-effect-unmount': + label += ' unmounted layout effect'; + break; + case 'passive-effect-mount': + label += ' mounted passive effect'; + break; + case 'passive-effect-unmount': + label += ' unmounted passive effect'; + break; + } return ( <> diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js index c7de8f8897c5e..a241498a05a05 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js @@ -76,7 +76,13 @@ export class ComponentMeasuresView extends View { showHoverHighlight: boolean, ): boolean { const {frame} = this; - const {componentName, duration, timestamp, warning} = componentMeasure; + const { + componentName, + duration, + timestamp, + type, + warning, + } = componentMeasure; const xStart = timestampToPosition(timestamp, scaleFactor, frame); const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame); @@ -96,6 +102,9 @@ export class ComponentMeasuresView extends View { return false; // Too small to render at this zoom level } + let textFillStyle = ((null: any): string); + let typeLabel = ((null: any): string); + const drawableRect = intersectionOfRects(componentMeasureRect, rect); context.beginPath(); if (warning !== null) { @@ -103,9 +112,43 @@ export class ComponentMeasuresView extends View { ? COLORS.WARNING_BACKGROUND_HOVER : COLORS.WARNING_BACKGROUND; } else { - context.fillStyle = showHoverHighlight - ? COLORS.REACT_COMPONENT_MEASURE_HOVER - : COLORS.REACT_COMPONENT_MEASURE; + switch (type) { + case 'render': + context.fillStyle = showHoverHighlight + ? COLORS.REACT_RENDER_HOVER + : COLORS.REACT_RENDER; + textFillStyle = COLORS.REACT_RENDER_TEXT; + typeLabel = 'rendered'; + break; + case 'layout-effect-mount': + context.fillStyle = showHoverHighlight + ? COLORS.REACT_LAYOUT_EFFECTS_HOVER + : COLORS.REACT_LAYOUT_EFFECTS; + textFillStyle = COLORS.REACT_LAYOUT_EFFECTS_TEXT; + typeLabel = 'mounted layout effect'; + break; + case 'layout-effect-unmount': + context.fillStyle = showHoverHighlight + ? COLORS.REACT_LAYOUT_EFFECTS_HOVER + : COLORS.REACT_LAYOUT_EFFECTS; + textFillStyle = COLORS.REACT_LAYOUT_EFFECTS_TEXT; + typeLabel = 'unmounted layout effect'; + break; + case 'passive-effect-mount': + context.fillStyle = showHoverHighlight + ? COLORS.REACT_PASSIVE_EFFECTS_HOVER + : COLORS.REACT_PASSIVE_EFFECTS; + textFillStyle = COLORS.REACT_PASSIVE_EFFECTS_TEXT; + typeLabel = 'mounted passive effect'; + break; + case 'passive-effect-unmount': + context.fillStyle = showHoverHighlight + ? COLORS.REACT_PASSIVE_EFFECTS_HOVER + : COLORS.REACT_PASSIVE_EFFECTS; + textFillStyle = COLORS.REACT_PASSIVE_EFFECTS_TEXT; + typeLabel = 'unmounted passive effect'; + break; + } } context.fillRect( drawableRect.origin.x, @@ -114,9 +157,11 @@ export class ComponentMeasuresView extends View { drawableRect.size.height, ); - const label = `${componentName} rendered - ${formatDuration(duration)}`; + const label = `${componentName} ${typeLabel} - ${formatDuration(duration)}`; - drawText(label, context, componentMeasureRect, drawableRect); + drawText(label, context, componentMeasureRect, drawableRect, { + fillStyle: textFillStyle, + }); return true; } diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js index 4d896d1967e96..d0895d2aff8e7 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -59,8 +59,6 @@ export let COLORS = { PRIORITY_LABEL: '', USER_TIMING: '', USER_TIMING_HOVER: '', - REACT_COMPONENT_MEASURE: '', - REACT_COMPONENT_MEASURE_HOVER: '', REACT_IDLE: '', REACT_IDLE_HOVER: '', REACT_RENDER: '', @@ -150,12 +148,6 @@ export function updateColorsToMatchTheme(element: Element): boolean { USER_TIMING_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-user-timing-hover', ), - REACT_COMPONENT_MEASURE: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-render', - ), - REACT_COMPONENT_MEASURE_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-render-hover', - ), REACT_IDLE: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-idle', ), diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js index f8690dc57447c..30cba586615c6 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js @@ -840,7 +840,7 @@ describe('preprocessData', () => { Object { "batchUID": 0, "depth": 0, - "duration": 0.0019999999999999983, + "duration": 0.004, "lanes": Array [ 4, ], @@ -852,11 +852,11 @@ describe('preprocessData', () => { Object { "batchUID": 1, "depth": 0, - "duration": 0.010000000000000002, + "duration": 0.009999999999999998, "lanes": Array [ 4, ], - "timestamp": 0.019, + "timestamp": 0.021, "type": "render-idle", }, Object { @@ -866,37 +866,37 @@ describe('preprocessData', () => { "lanes": Array [ 4, ], - "timestamp": 0.019, + "timestamp": 0.021, "type": "render", }, Object { "batchUID": 1, "depth": 0, - "duration": 0.006000000000000002, + "duration": 0.005999999999999998, "lanes": Array [ 4, ], - "timestamp": 0.023, + "timestamp": 0.025, "type": "commit", }, Object { "batchUID": 1, "depth": 1, - "duration": 0.0010000000000000009, + "duration": 0.0009999999999999974, "lanes": Array [ 4, ], - "timestamp": 0.027, + "timestamp": 0.029, "type": "layout-effects", }, Object { "batchUID": 1, "depth": 0, - "duration": 0.0010000000000000009, + "duration": 0.0030000000000000027, "lanes": Array [ 4, ], - "timestamp": 0.03, + "timestamp": 0.032, "type": "passive-effects", }, ], @@ -906,16 +906,32 @@ describe('preprocessData', () => { "componentName": "App", "duration": 0.001, "timestamp": 0.006, + "type": "render", + "warning": null, + }, + Object { + "componentName": "App", + "duration": 0.0019999999999999983, + "timestamp": 0.017, + "type": "passive-effect-mount", "warning": null, }, Object { "componentName": "App", "duration": 0.0010000000000000009, - "timestamp": 0.02, + "timestamp": 0.022, + "type": "render", + "warning": null, + }, + Object { + "componentName": "App", + "duration": 0.0010000000000000009, + "timestamp": 0.033, + "type": "passive-effect-mount", "warning": null, }, ], - "duration": 0.031, + "duration": 0.035, "flamechart": Array [], "internalModuleSourceToRanges": Map {}, "laneToLabelMap": Map { @@ -1000,7 +1016,7 @@ describe('preprocessData', () => { Object { "batchUID": 0, "depth": 0, - "duration": 0.0019999999999999983, + "duration": 0.004, "lanes": Array [ 4, ], @@ -1010,11 +1026,11 @@ describe('preprocessData', () => { Object { "batchUID": 1, "depth": 0, - "duration": 0.010000000000000002, + "duration": 0.009999999999999998, "lanes": Array [ 4, ], - "timestamp": 0.019, + "timestamp": 0.021, "type": "render-idle", }, Object { @@ -1024,37 +1040,37 @@ describe('preprocessData', () => { "lanes": Array [ 4, ], - "timestamp": 0.019, + "timestamp": 0.021, "type": "render", }, Object { "batchUID": 1, "depth": 0, - "duration": 0.006000000000000002, + "duration": 0.005999999999999998, "lanes": Array [ 4, ], - "timestamp": 0.023, + "timestamp": 0.025, "type": "commit", }, Object { "batchUID": 1, "depth": 1, - "duration": 0.0010000000000000009, + "duration": 0.0009999999999999974, "lanes": Array [ 4, ], - "timestamp": 0.027, + "timestamp": 0.029, "type": "layout-effects", }, Object { "batchUID": 1, "depth": 0, - "duration": 0.0010000000000000009, + "duration": 0.0030000000000000027, "lanes": Array [ 4, ], - "timestamp": 0.03, + "timestamp": 0.032, "type": "passive-effects", }, ], @@ -1108,7 +1124,7 @@ describe('preprocessData', () => { "lanes": Array [ 4, ], - "timestamp": 0.017, + "timestamp": 0.018, "type": "schedule-state-update", "warning": null, }, diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js index 6c41de1e3e279..e6bcfc178d003 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -22,6 +22,7 @@ import type { Phase, ReactLane, ReactComponentMeasure, + ReactComponentMeasureType, ReactMeasure, ReactMeasureType, ReactProfilerData, @@ -484,31 +485,13 @@ function processTimelineEvent( } else if (name.startsWith('--react-lane-labels-')) { const [laneLabelTuplesString] = name.substr(20).split('-'); updateLaneToLabelMap(currentProfilerData, laneLabelTuplesString); - } else if (name.startsWith('--component-render-start-')) { - const [componentName] = name.substr(25).split('-'); - - if (state.currentReactComponentMeasure !== null) { - console.error( - 'Render started while another render in progress:', - state.currentReactComponentMeasure, - ); - } - - state.currentReactComponentMeasure = { - componentName, - timestamp: startTime, - duration: 0, - warning: null, - }; - } else if (name === '--component-render-stop') { - if (state.currentReactComponentMeasure !== null) { - const componentMeasure = state.currentReactComponentMeasure; - componentMeasure.duration = startTime - componentMeasure.timestamp; - - state.currentReactComponentMeasure = null; - - currentProfilerData.componentMeasures.push(componentMeasure); - } + } else if (name.startsWith('--component-')) { + processReactComponentMeasure( + name, + startTime, + currentProfilerData, + state, + ); } else if (name.startsWith('--schedule-render-')) { const [laneBitmaskString] = name.substr(18).split('-'); @@ -868,6 +851,154 @@ function processTimelineEvent( } } +function assertNoOverlappingComponentMeasure(state: ProcessorState) { + if (state.currentReactComponentMeasure !== null) { + console.error( + 'Component measure started while another measure in progress:', + state.currentReactComponentMeasure, + ); + } +} + +function assertCurrentComponentMeasureType( + state: ProcessorState, + type: ReactComponentMeasureType, +): void { + if (state.currentReactComponentMeasure === null) { + console.error( + `Component measure type "${type}" stopped while no measure was in progress`, + ); + } else if (state.currentReactComponentMeasure.type !== type) { + console.error( + `Component measure type "${type}" stopped while type ${state.currentReactComponentMeasure.type} in progress`, + ); + } +} + +function processReactComponentMeasure( + name: string, + startTime: Milliseconds, + currentProfilerData: ReactProfilerData, + state: ProcessorState, +): void { + if (name.startsWith('--component-render-start-')) { + const [componentName] = name.substr(25).split('-'); + + assertNoOverlappingComponentMeasure(state); + + state.currentReactComponentMeasure = { + componentName, + timestamp: startTime, + duration: 0, + type: 'render', + warning: null, + }; + } else if (name === '--component-render-stop') { + assertCurrentComponentMeasureType(state, 'render'); + + if (state.currentReactComponentMeasure !== null) { + const componentMeasure = state.currentReactComponentMeasure; + componentMeasure.duration = startTime - componentMeasure.timestamp; + + state.currentReactComponentMeasure = null; + + currentProfilerData.componentMeasures.push(componentMeasure); + } + } else if (name.startsWith('--component-layout-effect-mount-start-')) { + const [componentName] = name.substr(38).split('-'); + + assertNoOverlappingComponentMeasure(state); + + state.currentReactComponentMeasure = { + componentName, + timestamp: startTime, + duration: 0, + type: 'layout-effect-mount', + warning: null, + }; + } else if (name === '--component-layout-effect-mount-stop') { + assertCurrentComponentMeasureType(state, 'layout-effect-mount'); + + if (state.currentReactComponentMeasure !== null) { + const componentMeasure = state.currentReactComponentMeasure; + componentMeasure.duration = startTime - componentMeasure.timestamp; + + state.currentReactComponentMeasure = null; + + currentProfilerData.componentMeasures.push(componentMeasure); + } + } else if (name.startsWith('--component-layout-effect-unmount-start-')) { + const [componentName] = name.substr(40).split('-'); + + assertNoOverlappingComponentMeasure(state); + + state.currentReactComponentMeasure = { + componentName, + timestamp: startTime, + duration: 0, + type: 'layout-effect-unmount', + warning: null, + }; + } else if (name === '--component-layout-effect-unmount-stop') { + assertCurrentComponentMeasureType(state, 'layout-effect-unmount'); + + if (state.currentReactComponentMeasure !== null) { + const componentMeasure = state.currentReactComponentMeasure; + componentMeasure.duration = startTime - componentMeasure.timestamp; + + state.currentReactComponentMeasure = null; + + currentProfilerData.componentMeasures.push(componentMeasure); + } + } else if (name.startsWith('--component-passive-effect-mount-start-')) { + const [componentName] = name.substr(39).split('-'); + + assertNoOverlappingComponentMeasure(state); + + state.currentReactComponentMeasure = { + componentName, + timestamp: startTime, + duration: 0, + type: 'passive-effect-mount', + warning: null, + }; + } else if (name === '--component-passive-effect-mount-stop') { + assertCurrentComponentMeasureType(state, 'passive-effect-mount'); + + if (state.currentReactComponentMeasure !== null) { + const componentMeasure = state.currentReactComponentMeasure; + componentMeasure.duration = startTime - componentMeasure.timestamp; + + state.currentReactComponentMeasure = null; + + currentProfilerData.componentMeasures.push(componentMeasure); + } + } else if (name.startsWith('--component-passive-effect-unmount-start-')) { + const [componentName] = name.substr(41).split('-'); + + assertNoOverlappingComponentMeasure(state); + + state.currentReactComponentMeasure = { + componentName, + timestamp: startTime, + duration: 0, + type: 'passive-effect-unmount', + warning: null, + }; + } else if (name === '--component-passive-effect-unmount-stop') { + assertCurrentComponentMeasureType(state, 'passive-effect-unmount'); + + if (state.currentReactComponentMeasure !== null) { + const componentMeasure = state.currentReactComponentMeasure; + componentMeasure.duration = startTime - componentMeasure.timestamp; + + state.currentReactComponentMeasure = null; + + currentProfilerData.componentMeasures.push(componentMeasure); + } + } +} + function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart { let parsedData; try { diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js index e5b14e7897ace..cf504229b01c5 100644 --- a/packages/react-devtools-scheduling-profiler/src/types.js +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -119,10 +119,18 @@ export type NetworkMeasure = {| url: string, |}; +export type ReactComponentMeasureType = + | 'render' + | 'layout-effect-mount' + | 'layout-effect-unmount' + | 'passive-effect-mount' + | 'passive-effect-unmount'; + export type ReactComponentMeasure = {| +componentName: string, duration: Milliseconds, +timestamp: Milliseconds, + +type: ReactComponentMeasureType, warning: string | null, |}; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 95c2f3753bfd7..235bd74361730 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -30,6 +30,7 @@ import { enableProfilerTimer, enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, + enableSchedulingProfiler, enableSuspenseServerRenderer, enableSuspenseCallback, enableScopeAPI, @@ -142,6 +143,16 @@ import { import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new'; import {doesFiberContain} from './ReactFiberTreeReflection'; import {invokeGuardedCallback, clearCaughtError} from 'shared/ReactErrorUtils'; +import { + markComponentPassiveEffectMountStarted, + markComponentPassiveEffectMountStopped, + markComponentPassiveEffectUnmountStarted, + markComponentPassiveEffectUnmountStopped, + markComponentLayoutEffectMountStarted, + markComponentLayoutEffectMountStopped, + markComponentLayoutEffectUnmountStarted, + markComponentLayoutEffectUnmountStopped, +} from './SchedulingProfiler'; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -512,7 +523,23 @@ function commitHookEffectListUnmount( const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectUnmountStarted(finishedWork); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectUnmountStarted(finishedWork); + } + } + safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy); + + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectUnmountStopped(); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectUnmountStopped(); + } + } } } effect = effect.next; @@ -520,18 +547,34 @@ function commitHookEffectListUnmount( } } -function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) { +function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { - if ((effect.tag & tag) === tag) { + if ((effect.tag & flags) === flags) { + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectMountStarted(finishedWork); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectMountStarted(finishedWork); + } + } + // Mount const create = effect.create; effect.destroy = create(); + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectMountStopped(); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectMountStopped(); + } + } + if (__DEV__) { const destroy = effect.destroy; if (destroy !== undefined && typeof destroy !== 'function') { @@ -1180,10 +1223,13 @@ function commitUnmount( do { const {destroy, tag} = effect; if (destroy !== undefined) { - if ( - (tag & HookInsertion) !== NoHookEffect || - (tag & HookLayout) !== NoHookEffect - ) { + if ((tag & HookInsertion) !== NoHookEffect) { + safelyCallDestroy(current, nearestMountedAncestor, destroy); + } else if ((tag & HookLayout) !== NoHookEffect) { + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStarted(current); + } + if ( enableProfilerTimer && enableProfilerCommitHooks && @@ -1195,6 +1241,10 @@ function commitUnmount( } else { safelyCallDestroy(current, nearestMountedAncestor, destroy); } + + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStopped(); + } } } effect = effect.next; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 8057c415e7459..c4da995064d29 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -30,6 +30,7 @@ import { enableProfilerTimer, enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, + enableSchedulingProfiler, enableSuspenseServerRenderer, enableSuspenseCallback, enableScopeAPI, @@ -142,6 +143,16 @@ import { import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old'; import {doesFiberContain} from './ReactFiberTreeReflection'; import {invokeGuardedCallback, clearCaughtError} from 'shared/ReactErrorUtils'; +import { + markComponentPassiveEffectMountStarted, + markComponentPassiveEffectMountStopped, + markComponentPassiveEffectUnmountStarted, + markComponentPassiveEffectUnmountStopped, + markComponentLayoutEffectMountStarted, + markComponentLayoutEffectMountStopped, + markComponentLayoutEffectUnmountStarted, + markComponentLayoutEffectUnmountStopped, +} from './SchedulingProfiler'; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -512,7 +523,23 @@ function commitHookEffectListUnmount( const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectUnmountStarted(finishedWork); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectUnmountStarted(finishedWork); + } + } + safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy); + + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectUnmountStopped(); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectUnmountStopped(); + } + } } } effect = effect.next; @@ -520,18 +547,34 @@ function commitHookEffectListUnmount( } } -function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) { +function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { - if ((effect.tag & tag) === tag) { + if ((effect.tag & flags) === flags) { + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectMountStarted(finishedWork); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectMountStarted(finishedWork); + } + } + // Mount const create = effect.create; effect.destroy = create(); + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectMountStopped(); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectMountStopped(); + } + } + if (__DEV__) { const destroy = effect.destroy; if (destroy !== undefined && typeof destroy !== 'function') { @@ -1180,10 +1223,13 @@ function commitUnmount( do { const {destroy, tag} = effect; if (destroy !== undefined) { - if ( - (tag & HookInsertion) !== NoHookEffect || - (tag & HookLayout) !== NoHookEffect - ) { + if ((tag & HookInsertion) !== NoHookEffect) { + safelyCallDestroy(current, nearestMountedAncestor, destroy); + } else if ((tag & HookLayout) !== NoHookEffect) { + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStarted(current); + } + if ( enableProfilerTimer && enableProfilerCommitHooks && @@ -1195,6 +1241,10 @@ function commitUnmount( } else { safelyCallDestroy(current, nearestMountedAncestor, destroy); } + + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStopped(); + } } } effect = effect.next; diff --git a/packages/react-reconciler/src/SchedulingProfiler.js b/packages/react-reconciler/src/SchedulingProfiler.js index 4df55137fda97..acfb2c3f976d0 100644 --- a/packages/react-reconciler/src/SchedulingProfiler.js +++ b/packages/react-reconciler/src/SchedulingProfiler.js @@ -161,6 +161,78 @@ export function markComponentRenderStopped(): void { } } +export function markComponentPassiveEffectMountStarted(fiber: Fiber): void { + if (enableSchedulingProfiler) { + if (supportsUserTimingV3) { + const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; + // TODO (scheduling profiler) Add component stack id + markAndClear(`--component-passive-effect-mount-start-${componentName}`); + } + } +} + +export function markComponentPassiveEffectMountStopped(): void { + if (enableSchedulingProfiler) { + if (supportsUserTimingV3) { + markAndClear('--component-passive-effect-mount-stop'); + } + } +} + +export function markComponentPassiveEffectUnmountStarted(fiber: Fiber): void { + if (enableSchedulingProfiler) { + if (supportsUserTimingV3) { + const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; + // TODO (scheduling profiler) Add component stack id + markAndClear(`--component-passive-effect-unmount-start-${componentName}`); + } + } +} + +export function markComponentPassiveEffectUnmountStopped(): void { + if (enableSchedulingProfiler) { + if (supportsUserTimingV3) { + markAndClear('--component-passive-effect-unmount-stop'); + } + } +} + +export function markComponentLayoutEffectMountStarted(fiber: Fiber): void { + if (enableSchedulingProfiler) { + if (supportsUserTimingV3) { + const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; + // TODO (scheduling profiler) Add component stack id + markAndClear(`--component-layout-effect-mount-start-${componentName}`); + } + } +} + +export function markComponentLayoutEffectMountStopped(): void { + if (enableSchedulingProfiler) { + if (supportsUserTimingV3) { + markAndClear('--component-layout-effect-mount-stop'); + } + } +} + +export function markComponentLayoutEffectUnmountStarted(fiber: Fiber): void { + if (enableSchedulingProfiler) { + if (supportsUserTimingV3) { + const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; + // TODO (scheduling profiler) Add component stack id + markAndClear(`--component-layout-effect-unmount-start-${componentName}`); + } + } +} + +export function markComponentLayoutEffectUnmountStopped(): void { + if (enableSchedulingProfiler) { + if (supportsUserTimingV3) { + markAndClear('--component-layout-effect-unmount-stop'); + } + } +} + export function markComponentErrored( fiber: Fiber, thrownValue: mixed, diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js index 5b58342ee0b2f..9b6ec83a40d39 100644 --- a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js +++ b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js @@ -608,30 +608,32 @@ describe('SchedulingProfiler', () => { if (gate(flags => flags.enableSchedulingProfiler)) { expect(getMarks()).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--schedule-state-update-1-Example", - "--layout-effects-stop", - "--render-start-1", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-1", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - "--commit-stop", - ] - `); + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--component-layout-effect-mount-start-Example", + "--schedule-state-update-1-Example", + "--component-layout-effect-mount-stop", + "--layout-effects-stop", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + "--commit-stop", + ] + `); } }); @@ -652,33 +654,35 @@ describe('SchedulingProfiler', () => { if (gate(flags => flags.enableSchedulingProfiler)) { expect(getMarks()).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--layout-effects-stop", - "--commit-stop", - "--passive-effects-start-16", - "--schedule-state-update-16-Example", - "--passive-effects-stop", - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - ] - `); + Array [ + "--schedule-render-16", + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + "--passive-effects-start-16", + "--component-passive-effect-mount-start-Example", + "--schedule-state-update-16-Example", + "--component-passive-effect-mount-stop", + "--passive-effects-stop", + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + ] + `); } }); @@ -854,4 +858,141 @@ describe('SchedulingProfiler', () => { `); } }); + + it('should mark passive and layout effects', async () => { + function ComponentWithEffects() { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('layout 1 mount'); + return () => { + Scheduler.unstable_yieldValue('layout 1 unmount'); + }; + }, []); + + React.useEffect(() => { + Scheduler.unstable_yieldValue('passive 1 mount'); + return () => { + Scheduler.unstable_yieldValue('passive 1 unmount'); + }; + }, []); + + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('layout 2 mount'); + return () => { + Scheduler.unstable_yieldValue('layout 2 unmount'); + }; + }, []); + + React.useEffect(() => { + Scheduler.unstable_yieldValue('passive 2 mount'); + return () => { + Scheduler.unstable_yieldValue('passive 2 unmount'); + }; + }, []); + + React.useEffect(() => { + Scheduler.unstable_yieldValue('passive 3 mount'); + return () => { + Scheduler.unstable_yieldValue('passive 3 unmount'); + }; + }, []); + + return null; + } + + const renderer = ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); + + expect(Scheduler).toFlushUntilNextPaint([ + 'layout 1 mount', + 'layout 2 mount', + ]); + + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + "--render-start-16", + "--component-render-start-ComponentWithEffects", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--component-layout-effect-mount-start-ComponentWithEffects", + "--component-layout-effect-mount-stop", + "--component-layout-effect-mount-start-ComponentWithEffects", + "--component-layout-effect-mount-stop", + "--layout-effects-stop", + "--commit-stop", + ] + `); + } + + clearPendingMarks(); + + expect(Scheduler).toFlushAndYield([ + 'passive 1 mount', + 'passive 2 mount', + 'passive 3 mount', + ]); + + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--passive-effects-start-16", + "--component-passive-effect-mount-start-ComponentWithEffects", + "--component-passive-effect-mount-stop", + "--component-passive-effect-mount-start-ComponentWithEffects", + "--component-passive-effect-mount-stop", + "--component-passive-effect-mount-start-ComponentWithEffects", + "--component-passive-effect-mount-stop", + "--passive-effects-stop", + ] + `); + } + + clearPendingMarks(); + + renderer.unmount(); + + expect(Scheduler).toFlushAndYield([ + 'layout 1 unmount', + 'layout 2 unmount', + 'passive 1 unmount', + 'passive 2 unmount', + 'passive 3 unmount', + ]); + + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + "--render-start-16", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--component-layout-effect-unmount-start-ComponentWithEffects", + "--component-layout-effect-unmount-stop", + "--component-layout-effect-unmount-start-ComponentWithEffects", + "--component-layout-effect-unmount-stop", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + "--passive-effects-start-16", + "--component-passive-effect-unmount-start-ComponentWithEffects", + "--component-passive-effect-unmount-stop", + "--component-passive-effect-unmount-start-ComponentWithEffects", + "--component-passive-effect-unmount-stop", + "--component-passive-effect-unmount-start-ComponentWithEffects", + "--component-passive-effect-unmount-stop", + "--passive-effects-stop", + ] + `); + } + }); }); From bfb40225b509dc65314f3fda95633090bf29ebf0 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 21 Oct 2021 15:16:26 -0400 Subject: [PATCH 058/109] Scheduling Profiler does not warn about long transitions (#22614) --- .../__tests__/preprocessData-test.internal.js | 137 ++++++++++++++++++ .../src/import-worker/preprocessData.js | 11 +- 2 files changed, 147 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js index 30cba586615c6..94ada274a770b 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js @@ -1601,6 +1601,143 @@ describe('preprocessData', () => { ); } }); + + it('should not warn about transition updates scheduled during commit phase', async () => { + function Component() { + const [value, setValue] = React.useState(0); + // eslint-disable-next-line no-unused-vars + const [isPending, startTransition] = React.useTransition(); + + Scheduler.unstable_yieldValue( + `Component rendered with value ${value}`, + ); + + // Fake a long render + if (value !== 0) { + Scheduler.unstable_yieldValue('Long render'); + startTime += 20000; + } + + React.useLayoutEffect(() => { + startTransition(() => { + setValue(1); + }); + }, []); + + return value; + } + + if (gate(flags => flags.enableSchedulingProfiler)) { + const cpuProfilerSample = creactCpuProfilerSample(); + + const root = ReactDOM.createRoot(document.createElement('div')); + act(() => { + root.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'Component rendered with value 0', + 'Component rendered with value 0', + 'Component rendered with value 1', + 'Long render', + ]); + + const testMarks = []; + clearedMarks.forEach(markName => { + if (markName === '--component-render-start-Component') { + // Fake a long running render + startTime += 20000; + } + + testMarks.push({ + pid: ++pid, + tid: ++tid, + ts: ++startTime, + args: {data: {}}, + cat: 'blink.user_timing', + name: markName, + ph: 'R', + }); + }); + + const data = await preprocessData([ + cpuProfilerSample, + ...createBoilerplateEntries(), + ...testMarks, + ]); + + data.schedulingEvents.forEach(event => { + expect(event.warning).toBeNull(); + }); + } + }); + + it('should not warn about deferred value updates scheduled during commit phase', async () => { + function Component() { + const [value, setValue] = React.useState(0); + const deferredValue = React.useDeferredValue(value); + + Scheduler.unstable_yieldValue( + `Component rendered with value ${value} and deferredValue ${deferredValue}`, + ); + + // Fake a long render + if (deferredValue !== 0) { + Scheduler.unstable_yieldValue('Long render'); + startTime += 20000; + } + + React.useLayoutEffect(() => { + setValue(1); + }, []); + + return value + deferredValue; + } + + if (gate(flags => flags.enableSchedulingProfiler)) { + const cpuProfilerSample = creactCpuProfilerSample(); + + const root = ReactDOM.createRoot(document.createElement('div')); + act(() => { + root.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'Component rendered with value 0 and deferredValue 0', + 'Component rendered with value 1 and deferredValue 0', + 'Component rendered with value 1 and deferredValue 1', + 'Long render', + ]); + + const testMarks = []; + clearedMarks.forEach(markName => { + if (markName === '--component-render-start-Component') { + // Fake a long running render + startTime += 20000; + } + + testMarks.push({ + pid: ++pid, + tid: ++tid, + ts: ++startTime, + args: {data: {}}, + cat: 'blink.user_timing', + name: markName, + ph: 'R', + }); + }); + + const data = await preprocessData([ + cpuProfilerSample, + ...createBoilerplateEntries(), + ...testMarks, + ]); + + data.schedulingEvents.forEach(event => { + expect(event.warning).toBeNull(); + }); + } + }); }); describe('errors thrown while rendering', () => { diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js index e6bcfc178d003..d6c911af1d076 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -1160,7 +1160,16 @@ export default async function preprocessData( // See how long the subsequent batch of React work was. const [startTime, stopTime] = getBatchRange(batchUID, profilerData); if (stopTime - startTime > NESTED_UPDATE_DURATION_THRESHOLD) { - schedulingEvent.warning = WARNING_STRINGS.NESTED_UPDATE; + // Don't warn about transition updates scheduled during the commit phase. + // e.g. useTransition, useDeferredValue + // These are allowed to be long-running. + if ( + !schedulingEvent.lanes.some( + lane => profilerData.laneToLabelMap.get(lane) === 'Transition', + ) + ) { + schedulingEvent.warning = WARNING_STRINGS.NESTED_UPDATE; + } } }); state.potentialSuspenseEventsOutsideOfTransition.forEach( From fa9bea0c41ccfef5b528ef9b5517607f9f94c52a Mon Sep 17 00:00:00 2001 From: Joseph Savona Date: Thu, 21 Oct 2021 14:11:42 -0700 Subject: [PATCH 059/109] Initial implementation of cache cleanup (#22510) This is an initial, partial implementation of a cleanup mechanism for the experimental Cache API. The idea is that consumers of the Cache API can register to be informed when a given Cache instance is no longer needed so that they can perform associated cleanup tasks to free resources stored in the cache. A canonical example would be cancelling pending network requests. An overview of the high-level changes: * Changes the `Cache` type from a Map of cache instances to be an object with the original Map of instances, a reference count (to count roughly "active references" to the cache instances - more below), and an AbortController. * Adds a new public API, `unstable_getCacheSignal(): AbortSignal`, which is callable during render. It returns an AbortSignal tied to the lifetime of the cache - developers can listen for the 'abort' event on the signal, which React now triggers when a given cache instance is no longer referenced. * Note that `AbortSignal` is a web standard that is supported by other platform APIs; for example a signal can be passed to `fetch()` to trigger cancellation of an HTTP request. * Implements the above - triggering the 'abort' event - by handling passive mount/unmount for HostRoot and CacheComponent fiber nodes. Cases handled: * Aborted transitions: we clean up a new cache created for an aborted transition * Suspense: we retain a fresh cache instance until a suspended tree resolves For follow-ups: * When a subsequent cache refresh is issued before a previous refresh completes, the refreshes are queued. Fresh cache instances for previous refreshes in the queue should be cleared, retaining only the most recent cache. I plan to address this in a follow-up PR. * If a refresh is cancelled, the fresh cache should be cleaned up. --- .../__snapshots__/profilingCache-test.js.snap | 4 +- .../src/server/ReactPartialRendererHooks.js | 5 + .../src/ReactFiberCacheComponent.new.js | 85 ++- .../src/ReactFiberCacheComponent.old.js | 85 ++- .../src/ReactFiberCommitWork.new.js | 117 ++++ .../src/ReactFiberCommitWork.old.js | 117 ++++ .../src/ReactFiberCompleteWork.new.js | 59 ++ .../src/ReactFiberCompleteWork.old.js | 59 ++ .../src/ReactFiberHooks.new.js | 35 +- .../src/ReactFiberHooks.old.js | 35 +- .../src/ReactFiberLane.new.js | 10 - .../src/ReactFiberLane.old.js | 10 - .../src/ReactFiberRoot.new.js | 13 +- .../src/ReactFiberRoot.old.js | 13 +- .../src/ReactFiberWorkLoop.new.js | 40 ++ .../src/ReactFiberWorkLoop.old.js | 40 ++ .../src/ReactInternalTypes.js | 1 + .../src/__tests__/ReactCache-test.js | 651 +++++++++++++++++- .../SchedulingProfiler-test.internal.js | 186 ++--- .../SchedulingProfilerLabels-test.internal.js | 98 +-- ...StrictEffectsModeDefaults-test.internal.js | 1 + packages/react/index.classic.fb.js | 1 + packages/react/index.experimental.js | 1 + packages/react/index.js | 1 + packages/react/index.modern.fb.js | 1 + packages/react/src/React.js | 2 + packages/react/src/ReactHooks.js | 6 + .../unstable-shared-subset.experimental.js | 1 + scripts/jest/setupEnvironment.js | 4 + 29 files changed, 1482 insertions(+), 199 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap index 1462a64195d6b..8d7eeb4255a94 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap @@ -73,7 +73,7 @@ Object { }, }, "duration": 15, - "effectDuration": null, + "effectDuration": 0, "fiberActualDurations": Map { 1 => 15, 2 => 15, @@ -86,7 +86,7 @@ Object { 3 => 3, 4 => 2, }, - "passiveEffectDuration": null, + "passiveEffectDuration": 0, "priorityLevel": "Immediate", "timestamp": 15, "updaters": Array [ diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index 6e2b715382a0a..168fd78f6103e 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -216,6 +216,10 @@ export function resetHooksState(): void { workInProgressHook = null; } +function getCacheSignal() { + throw new Error('Not implemented.'); +} + function getCacheForType(resourceType: () => T): T { throw new Error('Not implemented.'); } @@ -551,6 +555,7 @@ export const Dispatcher: DispatcherType = { }; if (enableCache) { + Dispatcher.getCacheSignal = getCacheSignal; Dispatcher.getCacheForType = getCacheForType; Dispatcher.useCacheRefresh = useCacheRefresh; } diff --git a/packages/react-reconciler/src/ReactFiberCacheComponent.new.js b/packages/react-reconciler/src/ReactFiberCacheComponent.new.js index 0d284c683ed1f..3ad60546c2b05 100644 --- a/packages/react-reconciler/src/ReactFiberCacheComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberCacheComponent.new.js @@ -18,8 +18,13 @@ import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import {isPrimaryRenderer} from './ReactFiberHostConfig'; import {createCursor, push, pop} from './ReactFiberStack.new'; import {pushProvider, popProvider} from './ReactFiberNewContext.new'; +import * as Scheduler from 'scheduler'; -export type Cache = Map<() => mixed, mixed>; +export type Cache = {| + controller: AbortController, + data: Map<() => mixed, mixed>, + refCount: number, +|}; export type CacheComponentState = {| +parent: Cache, @@ -31,6 +36,13 @@ export type SpawnedCachePool = {| +pool: Cache, |}; +// Intentionally not named imports because Rollup would +// use dynamic dispatch for CommonJS interop named imports. +const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, +} = Scheduler; + export const CacheContext: ReactContext = enableCache ? { $$typeof: REACT_CONTEXT_TYPE, @@ -57,6 +69,58 @@ let pooledCache: Cache | null = null; // cache from the render that suspended. const prevFreshCacheOnStack: StackCursor = createCursor(null); +// Creates a new empty Cache instance with a ref-count of 0. The caller is responsible +// for retaining the cache once it is in use (retainCache), and releasing the cache +// once it is no longer needed (releaseCache). +export function createCache(): Cache { + if (!enableCache) { + return (null: any); + } + const cache: Cache = { + controller: new AbortController(), + data: new Map(), + refCount: 0, + }; + + return cache; +} + +export function retainCache(cache: Cache) { + if (!enableCache) { + return; + } + if (__DEV__) { + if (cache.controller.signal.aborted) { + console.warn( + 'A cache instance was retained after it was already freed. ' + + 'This likely indicates a bug in React.', + ); + } + } + cache.refCount++; +} + +// Cleanup a cache instance, potentially freeing it if there are no more references +export function releaseCache(cache: Cache) { + if (!enableCache) { + return; + } + cache.refCount--; + if (__DEV__) { + if (cache.refCount < 0) { + console.warn( + 'A cache instance was released after it was already freed. ' + + 'This likely indicates a bug in React.', + ); + } + } + if (cache.refCount === 0) { + scheduleCallback(NormalPriority, () => { + cache.controller.abort(); + }); + } +} + export function pushCacheProvider(workInProgress: Fiber, cache: Cache) { if (!enableCache) { return; @@ -78,8 +142,14 @@ export function requestCacheFromPool(renderLanes: Lanes): Cache { if (pooledCache !== null) { return pooledCache; } - // Create a fresh cache. - pooledCache = new Map(); + // Create a fresh cache. The pooled cache must be owned - it is freed + // in releaseRootPooledCache() - but the cache instance handed out + // is retained/released in the commit phase of the component that + // references is (ie the host root, cache boundary, suspense component) + // Ie, pooledCache is conceptually an Option> (owned), + // whereas the return value of this function is a &Arc (borrowed). + pooledCache = createCache(); + retainCache(pooledCache); return pooledCache; } @@ -91,7 +161,13 @@ export function pushRootCachePool(root: FiberRoot) { // from `root.pooledCache`. If it's currently `null`, we will lazily // initialize it the first type it's requested. However, we only mutate // the root itself during the complete/unwind phase of the HostRoot. - pooledCache = root.pooledCache; + const rootCache = root.pooledCache; + if (rootCache != null) { + pooledCache = rootCache; + root.pooledCache = null; + } else { + pooledCache = null; + } } export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) { @@ -157,7 +233,6 @@ export function getSuspendedCachePool(): SpawnedCachePool | null { if (!enableCache) { return null; } - // We check the cache on the stack first, since that's the one any new Caches // would have accessed. let pool = pooledCache; diff --git a/packages/react-reconciler/src/ReactFiberCacheComponent.old.js b/packages/react-reconciler/src/ReactFiberCacheComponent.old.js index dd450fff76b50..a00059ededf10 100644 --- a/packages/react-reconciler/src/ReactFiberCacheComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberCacheComponent.old.js @@ -18,8 +18,13 @@ import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import {isPrimaryRenderer} from './ReactFiberHostConfig'; import {createCursor, push, pop} from './ReactFiberStack.old'; import {pushProvider, popProvider} from './ReactFiberNewContext.old'; +import * as Scheduler from 'scheduler'; -export type Cache = Map<() => mixed, mixed>; +export type Cache = {| + controller: AbortController, + data: Map<() => mixed, mixed>, + refCount: number, +|}; export type CacheComponentState = {| +parent: Cache, @@ -31,6 +36,13 @@ export type SpawnedCachePool = {| +pool: Cache, |}; +// Intentionally not named imports because Rollup would +// use dynamic dispatch for CommonJS interop named imports. +const { + unstable_scheduleCallback: scheduleCallback, + unstable_NormalPriority: NormalPriority, +} = Scheduler; + export const CacheContext: ReactContext = enableCache ? { $$typeof: REACT_CONTEXT_TYPE, @@ -57,6 +69,58 @@ let pooledCache: Cache | null = null; // cache from the render that suspended. const prevFreshCacheOnStack: StackCursor = createCursor(null); +// Creates a new empty Cache instance with a ref-count of 0. The caller is responsible +// for retaining the cache once it is in use (retainCache), and releasing the cache +// once it is no longer needed (releaseCache). +export function createCache(): Cache { + if (!enableCache) { + return (null: any); + } + const cache: Cache = { + controller: new AbortController(), + data: new Map(), + refCount: 0, + }; + + return cache; +} + +export function retainCache(cache: Cache) { + if (!enableCache) { + return; + } + if (__DEV__) { + if (cache.controller.signal.aborted) { + console.warn( + 'A cache instance was retained after it was already freed. ' + + 'This likely indicates a bug in React.', + ); + } + } + cache.refCount++; +} + +// Cleanup a cache instance, potentially freeing it if there are no more references +export function releaseCache(cache: Cache) { + if (!enableCache) { + return; + } + cache.refCount--; + if (__DEV__) { + if (cache.refCount < 0) { + console.warn( + 'A cache instance was released after it was already freed. ' + + 'This likely indicates a bug in React.', + ); + } + } + if (cache.refCount === 0) { + scheduleCallback(NormalPriority, () => { + cache.controller.abort(); + }); + } +} + export function pushCacheProvider(workInProgress: Fiber, cache: Cache) { if (!enableCache) { return; @@ -78,8 +142,14 @@ export function requestCacheFromPool(renderLanes: Lanes): Cache { if (pooledCache !== null) { return pooledCache; } - // Create a fresh cache. - pooledCache = new Map(); + // Create a fresh cache. The pooled cache must be owned - it is freed + // in releaseRootPooledCache() - but the cache instance handed out + // is retained/released in the commit phase of the component that + // references is (ie the host root, cache boundary, suspense component) + // Ie, pooledCache is conceptually an Option> (owned), + // whereas the return value of this function is a &Arc (borrowed). + pooledCache = createCache(); + retainCache(pooledCache); return pooledCache; } @@ -91,7 +161,13 @@ export function pushRootCachePool(root: FiberRoot) { // from `root.pooledCache`. If it's currently `null`, we will lazily // initialize it the first type it's requested. However, we only mutate // the root itself during the complete/unwind phase of the HostRoot. - pooledCache = root.pooledCache; + const rootCache = root.pooledCache; + if (rootCache != null) { + pooledCache = rootCache; + root.pooledCache = null; + } else { + pooledCache = null; + } } export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) { @@ -157,7 +233,6 @@ export function getSuspendedCachePool(): SpawnedCachePool | null { if (!enableCache) { return null; } - // We check the cache on the stack first, since that's the one any new Caches // would have accessed. let pool = pooledCache; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 235bd74361730..efdc515d9c0b2 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -24,6 +24,7 @@ import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; import type {Wakeable} from 'shared/ReactTypes'; import type {OffscreenState} from './ReactFiberOffscreenComponent'; import type {HookFlags} from './ReactHookEffectTags'; +import type {Cache} from './ReactFiberCacheComponent.new'; import { enableCreateEventHandleAPI, @@ -39,6 +40,7 @@ import { enableSuspenseLayoutEffectSemantics, enableUpdaterTracking, warnAboutCallbackRefReturningFunction, + enableCache, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -58,6 +60,7 @@ import { ScopeComponent, OffscreenComponent, LegacyHiddenComponent, + CacheComponent, } from './ReactWorkTags'; import {detachDeletedInstance} from './ReactFiberHostConfig'; import { @@ -153,6 +156,7 @@ import { markComponentLayoutEffectUnmountStarted, markComponentLayoutEffectUnmountStopped, } from './SchedulingProfiler'; +import {releaseCache, retainCache} from './ReactFiberCacheComponent.new'; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -2665,6 +2669,82 @@ function commitPassiveMountOnFiber( } break; } + case HostRoot: { + if (enableCache) { + let previousCache: Cache | null = null; + if (finishedWork.alternate !== null) { + previousCache = finishedWork.alternate.memoizedState.cache; + } + const nextCache = finishedWork.memoizedState.cache; + // Retain/release the root cache. + // Note that on initial mount, previousCache and nextCache will be the same + // and this retain won't occur. To counter this, we instead retain the HostRoot's + // initial cache when creating the root itself (see createFiberRoot() in + // ReactFiberRoot.js). Subsequent updates that change the cache are reflected + // here, such that previous/next caches are retained correctly. + if (nextCache !== previousCache) { + retainCache(nextCache); + if (previousCache != null) { + releaseCache(previousCache); + } + } + } + break; + } + case LegacyHiddenComponent: + case OffscreenComponent: { + if (enableCache) { + let previousCache: Cache | null = null; + if ( + finishedWork.alternate !== null && + finishedWork.alternate.memoizedState !== null && + finishedWork.alternate.memoizedState.cachePool !== null + ) { + previousCache = finishedWork.alternate.memoizedState.cachePool.pool; + } + let nextCache: Cache | null = null; + if ( + finishedWork.memoizedState !== null && + finishedWork.memoizedState.cachePool !== null + ) { + nextCache = finishedWork.memoizedState.cachePool.pool; + } + // Retain/release the cache used for pending (suspended) nodes. + // Note that this is only reached in the non-suspended/visible case: + // when the content is suspended/hidden, the retain/release occurs + // via the parent Suspense component (see case above). + if (nextCache !== previousCache) { + if (nextCache != null) { + retainCache(nextCache); + } + if (previousCache != null) { + releaseCache(previousCache); + } + } + } + break; + } + case CacheComponent: { + if (enableCache) { + let previousCache: Cache | null = null; + if (finishedWork.alternate !== null) { + previousCache = finishedWork.alternate.memoizedState.cache; + } + const nextCache = finishedWork.memoizedState.cache; + // Retain/release the cache. In theory the cache component + // could be "borrowing" a cache instance owned by some parent, + // in which case we could avoid retaining/releasing. But it + // is non-trivial to determine when that is the case, so we + // always retain/release. + if (nextCache !== previousCache) { + retainCache(nextCache); + if (previousCache != null) { + releaseCache(previousCache); + } + } + } + break; + } } } @@ -2871,6 +2951,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( } break; } + // TODO: run passive unmount effects when unmounting a root. + // Because passive unmount effects are not currently run, + // the cache instance owned by the root will never be freed. + // When effects are run, the cache should be freed here: + // case HostRoot: { + // if (enableCache) { + // const cache = current.memoizedState.cache; + // releaseCache(cache); + // } + // break; + // } + case LegacyHiddenComponent: + case OffscreenComponent: { + if (enableCache) { + if ( + current.memoizedState !== null && + current.memoizedState.cachePool !== null + ) { + const cache: Cache = current.memoizedState.cachePool.pool; + // Retain/release the cache used for pending (suspended) nodes. + // Note that this is only reached in the non-suspended/visible case: + // when the content is suspended/hidden, the retain/release occurs + // via the parent Suspense component (see case above). + if (cache != null) { + retainCache(cache); + } + } + } + break; + } + case CacheComponent: { + if (enableCache) { + const cache = current.memoizedState.cache; + releaseCache(cache); + } + break; + } } } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index c4da995064d29..7d4d5d7aeecef 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -24,6 +24,7 @@ import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old'; import type {Wakeable} from 'shared/ReactTypes'; import type {OffscreenState} from './ReactFiberOffscreenComponent'; import type {HookFlags} from './ReactHookEffectTags'; +import type {Cache} from './ReactFiberCacheComponent.old'; import { enableCreateEventHandleAPI, @@ -39,6 +40,7 @@ import { enableSuspenseLayoutEffectSemantics, enableUpdaterTracking, warnAboutCallbackRefReturningFunction, + enableCache, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -58,6 +60,7 @@ import { ScopeComponent, OffscreenComponent, LegacyHiddenComponent, + CacheComponent, } from './ReactWorkTags'; import {detachDeletedInstance} from './ReactFiberHostConfig'; import { @@ -153,6 +156,7 @@ import { markComponentLayoutEffectUnmountStarted, markComponentLayoutEffectUnmountStopped, } from './SchedulingProfiler'; +import {releaseCache, retainCache} from './ReactFiberCacheComponent.old'; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -2665,6 +2669,82 @@ function commitPassiveMountOnFiber( } break; } + case HostRoot: { + if (enableCache) { + let previousCache: Cache | null = null; + if (finishedWork.alternate !== null) { + previousCache = finishedWork.alternate.memoizedState.cache; + } + const nextCache = finishedWork.memoizedState.cache; + // Retain/release the root cache. + // Note that on initial mount, previousCache and nextCache will be the same + // and this retain won't occur. To counter this, we instead retain the HostRoot's + // initial cache when creating the root itself (see createFiberRoot() in + // ReactFiberRoot.js). Subsequent updates that change the cache are reflected + // here, such that previous/next caches are retained correctly. + if (nextCache !== previousCache) { + retainCache(nextCache); + if (previousCache != null) { + releaseCache(previousCache); + } + } + } + break; + } + case LegacyHiddenComponent: + case OffscreenComponent: { + if (enableCache) { + let previousCache: Cache | null = null; + if ( + finishedWork.alternate !== null && + finishedWork.alternate.memoizedState !== null && + finishedWork.alternate.memoizedState.cachePool !== null + ) { + previousCache = finishedWork.alternate.memoizedState.cachePool.pool; + } + let nextCache: Cache | null = null; + if ( + finishedWork.memoizedState !== null && + finishedWork.memoizedState.cachePool !== null + ) { + nextCache = finishedWork.memoizedState.cachePool.pool; + } + // Retain/release the cache used for pending (suspended) nodes. + // Note that this is only reached in the non-suspended/visible case: + // when the content is suspended/hidden, the retain/release occurs + // via the parent Suspense component (see case above). + if (nextCache !== previousCache) { + if (nextCache != null) { + retainCache(nextCache); + } + if (previousCache != null) { + releaseCache(previousCache); + } + } + } + break; + } + case CacheComponent: { + if (enableCache) { + let previousCache: Cache | null = null; + if (finishedWork.alternate !== null) { + previousCache = finishedWork.alternate.memoizedState.cache; + } + const nextCache = finishedWork.memoizedState.cache; + // Retain/release the cache. In theory the cache component + // could be "borrowing" a cache instance owned by some parent, + // in which case we could avoid retaining/releasing. But it + // is non-trivial to determine when that is the case, so we + // always retain/release. + if (nextCache !== previousCache) { + retainCache(nextCache); + if (previousCache != null) { + releaseCache(previousCache); + } + } + } + break; + } } } @@ -2871,6 +2951,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( } break; } + // TODO: run passive unmount effects when unmounting a root. + // Because passive unmount effects are not currently run, + // the cache instance owned by the root will never be freed. + // When effects are run, the cache should be freed here: + // case HostRoot: { + // if (enableCache) { + // const cache = current.memoizedState.cache; + // releaseCache(cache); + // } + // break; + // } + case LegacyHiddenComponent: + case OffscreenComponent: { + if (enableCache) { + if ( + current.memoizedState !== null && + current.memoizedState.cachePool !== null + ) { + const cache: Cache = current.memoizedState.cachePool.pool; + // Retain/release the cache used for pending (suspended) nodes. + // Note that this is only reached in the non-suspended/visible case: + // when the content is suspended/hidden, the retain/release occurs + // via the parent Suspense component (see case above). + if (cache != null) { + retainCache(cache); + } + } + } + break; + } + case CacheComponent: { + if (enableCache) { + const cache = current.memoizedState.cache; + releaseCache(cache); + } + break; + } } } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index dadec516c3a4b..20a7fc52db13a 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -72,6 +72,7 @@ import { ChildDeletion, StaticMask, MutationMask, + Passive, } from './ReactFiberFlags'; import { @@ -848,7 +849,15 @@ function completeWork( if (enableCache) { popRootCachePool(fiberRoot, renderLanes); + let previousCache: Cache | null = null; + if (workInProgress.alternate !== null) { + previousCache = workInProgress.alternate.memoizedState.cache; + } const cache: Cache = workInProgress.memoizedState.cache; + if (cache !== previousCache) { + // Run passive effects to retain/release the cache. + workInProgress.flags |= Passive; + } popCacheProvider(workInProgress, cache); } popHostContainer(workInProgress); @@ -1089,6 +1098,29 @@ function completeWork( prevDidTimeout = prevState !== null; } + if (enableCache && nextDidTimeout) { + const offscreenFiber: Fiber = (workInProgress.child: any); + let previousCache: Cache | null = null; + if ( + offscreenFiber.alternate !== null && + offscreenFiber.alternate.memoizedState !== null && + offscreenFiber.alternate.memoizedState.cachePool !== null + ) { + previousCache = offscreenFiber.alternate.memoizedState.cachePool.pool; + } + let cache: Cache | null = null; + if ( + offscreenFiber.memoizedState !== null && + offscreenFiber.memoizedState.cachePool !== null + ) { + cache = offscreenFiber.memoizedState.cachePool.pool; + } + if (cache !== previousCache) { + // Run passive effects to retain/release the cache. + offscreenFiber.flags |= Passive; + } + } + // If the suspended state of the boundary changes, we need to schedule // an effect to toggle the subtree's visibility. When we switch from // fallback -> primary, the inner Offscreen fiber schedules this effect @@ -1465,6 +1497,25 @@ function completeWork( } if (enableCache) { + let previousCache: Cache | null = null; + if ( + workInProgress.alternate !== null && + workInProgress.alternate.memoizedState !== null && + workInProgress.alternate.memoizedState.cachePool !== null + ) { + previousCache = workInProgress.alternate.memoizedState.cachePool.pool; + } + let cache: Cache | null = null; + if ( + workInProgress.memoizedState !== null && + workInProgress.memoizedState.cachePool !== null + ) { + cache = workInProgress.memoizedState.cachePool.pool; + } + if (cache !== previousCache) { + // Run passive effects to retain/release the cache. + workInProgress.flags |= Passive; + } const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any); if (spawnedCachePool !== null) { popCachePool(workInProgress); @@ -1475,7 +1526,15 @@ function completeWork( } case CacheComponent: { if (enableCache) { + let previousCache: Cache | null = null; + if (workInProgress.alternate !== null) { + previousCache = workInProgress.alternate.memoizedState.cache; + } const cache: Cache = workInProgress.memoizedState.cache; + if (cache !== previousCache) { + // Run passive effects to retain/release the cache. + workInProgress.flags |= Passive; + } popCacheProvider(workInProgress, cache); bubbleProperties(workInProgress); return null; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index 06fbf5abff50f..305359aef206e 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -72,6 +72,7 @@ import { ChildDeletion, StaticMask, MutationMask, + Passive, } from './ReactFiberFlags'; import { @@ -848,7 +849,15 @@ function completeWork( if (enableCache) { popRootCachePool(fiberRoot, renderLanes); + let previousCache: Cache | null = null; + if (workInProgress.alternate !== null) { + previousCache = workInProgress.alternate.memoizedState.cache; + } const cache: Cache = workInProgress.memoizedState.cache; + if (cache !== previousCache) { + // Run passive effects to retain/release the cache. + workInProgress.flags |= Passive; + } popCacheProvider(workInProgress, cache); } popHostContainer(workInProgress); @@ -1089,6 +1098,29 @@ function completeWork( prevDidTimeout = prevState !== null; } + if (enableCache && nextDidTimeout) { + const offscreenFiber: Fiber = (workInProgress.child: any); + let previousCache: Cache | null = null; + if ( + offscreenFiber.alternate !== null && + offscreenFiber.alternate.memoizedState !== null && + offscreenFiber.alternate.memoizedState.cachePool !== null + ) { + previousCache = offscreenFiber.alternate.memoizedState.cachePool.pool; + } + let cache: Cache | null = null; + if ( + offscreenFiber.memoizedState !== null && + offscreenFiber.memoizedState.cachePool !== null + ) { + cache = offscreenFiber.memoizedState.cachePool.pool; + } + if (cache !== previousCache) { + // Run passive effects to retain/release the cache. + offscreenFiber.flags |= Passive; + } + } + // If the suspended state of the boundary changes, we need to schedule // an effect to toggle the subtree's visibility. When we switch from // fallback -> primary, the inner Offscreen fiber schedules this effect @@ -1465,6 +1497,25 @@ function completeWork( } if (enableCache) { + let previousCache: Cache | null = null; + if ( + workInProgress.alternate !== null && + workInProgress.alternate.memoizedState !== null && + workInProgress.alternate.memoizedState.cachePool !== null + ) { + previousCache = workInProgress.alternate.memoizedState.cachePool.pool; + } + let cache: Cache | null = null; + if ( + workInProgress.memoizedState !== null && + workInProgress.memoizedState.cachePool !== null + ) { + cache = workInProgress.memoizedState.cachePool.pool; + } + if (cache !== previousCache) { + // Run passive effects to retain/release the cache. + workInProgress.flags |= Passive; + } const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any); if (spawnedCachePool !== null) { popCachePool(workInProgress); @@ -1475,7 +1526,15 @@ function completeWork( } case CacheComponent: { if (enableCache) { + let previousCache: Cache | null = null; + if (workInProgress.alternate !== null) { + previousCache = workInProgress.alternate.memoizedState.cache; + } const cache: Cache = workInProgress.memoizedState.cache; + if (cache !== previousCache) { + // Run passive effects to retain/release the cache. + workInProgress.flags |= Passive; + } popCacheProvider(workInProgress, cache); bubbleProperties(workInProgress); return null; diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index afaebd20a5264..d21704a1b6fea 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -110,7 +110,7 @@ import { import {getIsRendering} from './ReactCurrentFiber'; import {logStateUpdateScheduled} from './DebugTracing'; import {markStateUpdateScheduled} from './SchedulingProfiler'; -import {CacheContext} from './ReactFiberCacheComponent.new'; +import {createCache, CacheContext} from './ReactFiberCacheComponent.new'; import { createUpdate as createLegacyQueueUpdate, enqueueUpdate as enqueueLegacyQueueUpdate, @@ -2124,6 +2124,9 @@ function updateRefresh() { } function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { + if (!enableCache) { + return; + } // TODO: Does Cache work in legacy mode? Should decide and write a test. // TODO: Consider warning if the refresh is at discrete priority, or if we // otherwise suspect that it wasn't batched properly. @@ -2139,11 +2142,14 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { entangleLegacyQueueTransitions(root, provider, lane); } - const seededCache = new Map(); + // TODO: If a refresh never commits, the new cache created here must be + // released. A simple case is start refreshing a cache boundary, but then + // unmount that boundary before the refresh completes. + const seededCache = createCache(); if (seedKey !== null && seedKey !== undefined && root !== null) { // Seed the cache with the value passed by the caller. This could be // from a server mutation, or it could be a streaming response. - seededCache.set(seedKey, seedValue); + seededCache.data.set(seedKey, seedValue); } // Schedule an update on the cache boundary to trigger a refresh. @@ -2390,15 +2396,23 @@ function markUpdateInDevTools(fiber, lane, action) { } } +function getCacheSignal(): AbortSignal { + if (!enableCache) { + throw new Error('Not implemented.'); + } + const cache: Cache = readContext(CacheContext); + return cache.controller.signal; +} + function getCacheForType(resourceType: () => T): T { if (!enableCache) { throw new Error('Not implemented.'); } const cache: Cache = readContext(CacheContext); - let cacheForType: T | void = (cache.get(resourceType): any); + let cacheForType: T | void = (cache.data.get(resourceType): any); if (cacheForType === undefined) { cacheForType = resourceType(); - cache.set(resourceType, cacheForType); + cache.data.set(resourceType, cacheForType); } return cacheForType; } @@ -2426,6 +2440,7 @@ export const ContextOnlyDispatcher: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (ContextOnlyDispatcher: Dispatcher).getCacheSignal = getCacheSignal; (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType; (ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError; } @@ -2453,6 +2468,7 @@ const HooksDispatcherOnMount: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnMount: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh; } @@ -2480,6 +2496,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnUpdate: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh; } @@ -2507,6 +2524,7 @@ const HooksDispatcherOnRerender: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnRerender: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh; } @@ -2677,6 +2695,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -2818,6 +2837,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -2959,6 +2979,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -3101,6 +3122,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -3259,6 +3281,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal; (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -3417,6 +3440,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal; (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -3576,6 +3600,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal; (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 03df5b1444e62..cf6786e617daf 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -110,7 +110,7 @@ import { import {getIsRendering} from './ReactCurrentFiber'; import {logStateUpdateScheduled} from './DebugTracing'; import {markStateUpdateScheduled} from './SchedulingProfiler'; -import {CacheContext} from './ReactFiberCacheComponent.old'; +import {createCache, CacheContext} from './ReactFiberCacheComponent.old'; import { createUpdate as createLegacyQueueUpdate, enqueueUpdate as enqueueLegacyQueueUpdate, @@ -2124,6 +2124,9 @@ function updateRefresh() { } function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { + if (!enableCache) { + return; + } // TODO: Does Cache work in legacy mode? Should decide and write a test. // TODO: Consider warning if the refresh is at discrete priority, or if we // otherwise suspect that it wasn't batched properly. @@ -2139,11 +2142,14 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { entangleLegacyQueueTransitions(root, provider, lane); } - const seededCache = new Map(); + // TODO: If a refresh never commits, the new cache created here must be + // released. A simple case is start refreshing a cache boundary, but then + // unmount that boundary before the refresh completes. + const seededCache = createCache(); if (seedKey !== null && seedKey !== undefined && root !== null) { // Seed the cache with the value passed by the caller. This could be // from a server mutation, or it could be a streaming response. - seededCache.set(seedKey, seedValue); + seededCache.data.set(seedKey, seedValue); } // Schedule an update on the cache boundary to trigger a refresh. @@ -2390,15 +2396,23 @@ function markUpdateInDevTools(fiber, lane, action) { } } +function getCacheSignal(): AbortSignal { + if (!enableCache) { + throw new Error('Not implemented.'); + } + const cache: Cache = readContext(CacheContext); + return cache.controller.signal; +} + function getCacheForType(resourceType: () => T): T { if (!enableCache) { throw new Error('Not implemented.'); } const cache: Cache = readContext(CacheContext); - let cacheForType: T | void = (cache.get(resourceType): any); + let cacheForType: T | void = (cache.data.get(resourceType): any); if (cacheForType === undefined) { cacheForType = resourceType(); - cache.set(resourceType, cacheForType); + cache.data.set(resourceType, cacheForType); } return cacheForType; } @@ -2426,6 +2440,7 @@ export const ContextOnlyDispatcher: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (ContextOnlyDispatcher: Dispatcher).getCacheSignal = getCacheSignal; (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType; (ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError; } @@ -2453,6 +2468,7 @@ const HooksDispatcherOnMount: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnMount: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh; } @@ -2480,6 +2496,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnUpdate: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh; } @@ -2507,6 +2524,7 @@ const HooksDispatcherOnRerender: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnRerender: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh; } @@ -2677,6 +2695,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -2818,6 +2837,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -2959,6 +2979,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -3101,6 +3122,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal; (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -3259,6 +3281,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal; (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -3417,6 +3440,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal; (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; @@ -3576,6 +3600,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; if (enableCache) { + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal; (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index ad124a432a4a2..c1f34e1052fc4 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -17,7 +17,6 @@ export type Lane = number; export type LaneMap = Array; import { - enableCache, enableSchedulingProfiler, enableUpdaterTracking, allowConcurrentByDefault, @@ -635,15 +634,6 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) { root.entangledLanes &= remainingLanes; - if (enableCache) { - const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes); - if (pooledCacheLanes === NoLanes) { - // None of the remaining work relies on the cache pool. Clear it so - // subsequent requests get a new cache. - root.pooledCache = null; - } - } - const entanglements = root.entanglements; const eventTimes = root.eventTimes; const expirationTimes = root.expirationTimes; diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index 4a064a3846515..c81191f6a07e5 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -17,7 +17,6 @@ export type Lane = number; export type LaneMap = Array; import { - enableCache, enableSchedulingProfiler, enableUpdaterTracking, allowConcurrentByDefault, @@ -635,15 +634,6 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) { root.entangledLanes &= remainingLanes; - if (enableCache) { - const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes); - if (pooledCacheLanes === NoLanes) { - // None of the remaining work relies on the cache pool. Clear it so - // subsequent requests get a new cache. - root.pooledCache = null; - } - } - const entanglements = root.entanglements; const eventTimes = root.eventTimes; const expirationTimes = root.expirationTimes; diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index db012ad1db19d..803adee1e22dd 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -28,6 +28,7 @@ import { } from 'shared/ReactFeatureFlags'; import {initializeUpdateQueue} from './ReactUpdateQueue.new'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; +import {createCache, retainCache} from './ReactFiberCacheComponent.new'; function FiberRootNode(containerInfo, tag, hydrate) { this.tag = tag; @@ -117,8 +118,18 @@ export function createFiberRoot( uninitializedFiber.stateNode = root; if (enableCache) { - const initialCache = new Map(); + const initialCache = createCache(); + retainCache(initialCache); + + // The pooledCache is a fresh cache instance that is used temporarily + // for newly mounted boundaries during a render. In general, the + // pooledCache is always cleared from the root at the end of a render: + // it is either released when render commits, or moved to an Offscreen + // component if rendering suspends. Because the lifetime of the pooled + // cache is distinct from the main memoizedState.cache, it must be + // retained separately. root.pooledCache = initialCache; + retainCache(initialCache); const initialState = { element: null, cache: initialCache, diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js index 03332fd545619..504dac966ef22 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.old.js +++ b/packages/react-reconciler/src/ReactFiberRoot.old.js @@ -28,6 +28,7 @@ import { } from 'shared/ReactFeatureFlags'; import {initializeUpdateQueue} from './ReactUpdateQueue.old'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; +import {createCache, retainCache} from './ReactFiberCacheComponent.old'; function FiberRootNode(containerInfo, tag, hydrate) { this.tag = tag; @@ -117,8 +118,18 @@ export function createFiberRoot( uninitializedFiber.stateNode = root; if (enableCache) { - const initialCache = new Map(); + const initialCache = createCache(); + retainCache(initialCache); + + // The pooledCache is a fresh cache instance that is used temporarily + // for newly mounted boundaries during a render. In general, the + // pooledCache is always cleared from the root at the end of a render: + // it is either released when render commits, or moved to an Offscreen + // component if rendering suspends. Because the lifetime of the pooled + // cache is distinct from the main memoizedState.cache, it must be + // retained separately. root.pooledCache = initialCache; + retainCache(initialCache); const initialState = { element: null, cache: initialCache, diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 89286bd573858..930fc608f4724 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -32,6 +32,7 @@ import { skipUnmountedBoundaries, enableUpdaterTracking, warnOnSubscriptionInsideStartTransition, + enableCache, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import is from 'shared/objectIs'; @@ -234,6 +235,7 @@ import { isDevToolsPresent, } from './ReactFiberDevToolsHook.new'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; +import {releaseCache} from './ReactFiberCacheComponent.new'; const ceil = Math.ceil; @@ -331,6 +333,7 @@ let rootDoesHavePassiveEffects: boolean = false; let rootWithPendingPassiveEffects: FiberRoot | null = null; let pendingPassiveEffectsLanes: Lanes = NoLanes; let pendingPassiveProfilerEffects: Array = []; +let pendingPassiveEffectsRemainingLanes: Lanes = NoLanes; // Use these to prevent an infinite loop of nested updates const NESTED_UPDATE_LIMIT = 50; @@ -1900,8 +1903,12 @@ function commitRootImpl(root, renderPriorityLevel) { ) { if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; + pendingPassiveEffectsRemainingLanes = remainingLanes; scheduleCallback(NormalSchedulerPriority, () => { flushPassiveEffects(); + // This render triggered passive effects: release the root cache pool + // *after* passive effects fire to avoid freeing a cache pool that may + // be referenced by a node in the tree (HostRoot, Cache boundary etc) return null; }); } @@ -2027,6 +2034,10 @@ function commitRootImpl(root, renderPriorityLevel) { rootDoesHavePassiveEffects = false; rootWithPendingPassiveEffects = root; pendingPassiveEffectsLanes = lanes; + } else { + // There were no passive effects, so we can immediately release the cache + // pool for this render. + releaseRootPooledCache(root, remainingLanes); } // Read this again, since an effect might have updated it @@ -2127,6 +2138,21 @@ function commitRootImpl(root, renderPriorityLevel) { return null; } +function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) { + if (enableCache) { + const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes); + if (pooledCacheLanes === NoLanes) { + // None of the remaining work relies on the cache pool. Clear it so + // subsequent requests get a new cache + const pooledCache = root.pooledCache; + if (pooledCache != null) { + root.pooledCache = null; + releaseCache(pooledCache); + } + } + } +} + export function flushPassiveEffects(): boolean { // Returns whether passive effects were flushed. // TODO: Combine this check with the one in flushPassiveEFfectsImpl. We should @@ -2135,6 +2161,15 @@ export function flushPassiveEffects(): boolean { // `Scheduler.runWithPriority`, which accepts a function. But now we track the // priority within React itself, so we can mutate the variable directly. if (rootWithPendingPassiveEffects !== null) { + // Cache the root since rootWithPendingPassiveEffects is cleared in + // flushPassiveEffectsImpl + const root = rootWithPendingPassiveEffects; + // Cache and clear the remaining lanes flag; it must be reset since this + // method can be called from various places, not always from commitRoot + // where the remaining lanes are known + const remainingLanes = pendingPassiveEffectsRemainingLanes; + pendingPassiveEffectsRemainingLanes = NoLanes; + const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes); const priority = lowerEventPriority(DefaultEventPriority, renderPriority); const prevTransition = ReactCurrentBatchConfig.transition; @@ -2146,6 +2181,11 @@ export function flushPassiveEffects(): boolean { } finally { setCurrentUpdatePriority(previousPriority); ReactCurrentBatchConfig.transition = prevTransition; + + // Once passive effects have run for the tree - giving components a + // chance to retain cache instances they use - release the pooled + // cache at the root (if there is one) + releaseRootPooledCache(root, remainingLanes); } } return false; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index d5cb23483d2d0..0b7f46c799016 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -32,6 +32,7 @@ import { skipUnmountedBoundaries, enableUpdaterTracking, warnOnSubscriptionInsideStartTransition, + enableCache, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import is from 'shared/objectIs'; @@ -234,6 +235,7 @@ import { isDevToolsPresent, } from './ReactFiberDevToolsHook.old'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; +import {releaseCache} from './ReactFiberCacheComponent.old'; const ceil = Math.ceil; @@ -331,6 +333,7 @@ let rootDoesHavePassiveEffects: boolean = false; let rootWithPendingPassiveEffects: FiberRoot | null = null; let pendingPassiveEffectsLanes: Lanes = NoLanes; let pendingPassiveProfilerEffects: Array = []; +let pendingPassiveEffectsRemainingLanes: Lanes = NoLanes; // Use these to prevent an infinite loop of nested updates const NESTED_UPDATE_LIMIT = 50; @@ -1900,8 +1903,12 @@ function commitRootImpl(root, renderPriorityLevel) { ) { if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; + pendingPassiveEffectsRemainingLanes = remainingLanes; scheduleCallback(NormalSchedulerPriority, () => { flushPassiveEffects(); + // This render triggered passive effects: release the root cache pool + // *after* passive effects fire to avoid freeing a cache pool that may + // be referenced by a node in the tree (HostRoot, Cache boundary etc) return null; }); } @@ -2027,6 +2034,10 @@ function commitRootImpl(root, renderPriorityLevel) { rootDoesHavePassiveEffects = false; rootWithPendingPassiveEffects = root; pendingPassiveEffectsLanes = lanes; + } else { + // There were no passive effects, so we can immediately release the cache + // pool for this render. + releaseRootPooledCache(root, remainingLanes); } // Read this again, since an effect might have updated it @@ -2127,6 +2138,21 @@ function commitRootImpl(root, renderPriorityLevel) { return null; } +function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) { + if (enableCache) { + const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes); + if (pooledCacheLanes === NoLanes) { + // None of the remaining work relies on the cache pool. Clear it so + // subsequent requests get a new cache + const pooledCache = root.pooledCache; + if (pooledCache != null) { + root.pooledCache = null; + releaseCache(pooledCache); + } + } + } +} + export function flushPassiveEffects(): boolean { // Returns whether passive effects were flushed. // TODO: Combine this check with the one in flushPassiveEFfectsImpl. We should @@ -2135,6 +2161,15 @@ export function flushPassiveEffects(): boolean { // `Scheduler.runWithPriority`, which accepts a function. But now we track the // priority within React itself, so we can mutate the variable directly. if (rootWithPendingPassiveEffects !== null) { + // Cache the root since rootWithPendingPassiveEffects is cleared in + // flushPassiveEffectsImpl + const root = rootWithPendingPassiveEffects; + // Cache and clear the remaining lanes flag; it must be reset since this + // method can be called from various places, not always from commitRoot + // where the remaining lanes are known + const remainingLanes = pendingPassiveEffectsRemainingLanes; + pendingPassiveEffectsRemainingLanes = NoLanes; + const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes); const priority = lowerEventPriority(DefaultEventPriority, renderPriority); const prevTransition = ReactCurrentBatchConfig.transition; @@ -2146,6 +2181,11 @@ export function flushPassiveEffects(): boolean { } finally { setCurrentUpdatePriority(previousPriority); ReactCurrentBatchConfig.transition = prevTransition; + + // Once passive effects have run for the tree - giving components a + // chance to retain cache instances they use - release the pooled + // cache at the root (if there is one) + releaseRootPooledCache(root, remainingLanes); } } return false; diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index f079b3e8f2b93..e971828233c10 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -273,6 +273,7 @@ type BasicStateAction = (S => S) | S; type Dispatch = A => void; export type Dispatcher = {| + getCacheSignal?: () => AbortSignal, getCacheForType?: (resourceType: () => T) => T, readContext(context: ReactContext): T, useState(initialState: (() => S) | S): [S, Dispatch>], diff --git a/packages/react-reconciler/src/__tests__/ReactCache-test.js b/packages/react-reconciler/src/__tests__/ReactCache-test.js index 31321eb07a520..7ef18875e087d 100644 --- a/packages/react-reconciler/src/__tests__/ReactCache-test.js +++ b/packages/react-reconciler/src/__tests__/ReactCache-test.js @@ -1,6 +1,7 @@ let React; let ReactNoop; let Cache; +let getCacheSignal; let getCacheForType; let Scheduler; let act; @@ -22,6 +23,7 @@ describe('ReactCache', () => { Scheduler = require('scheduler'); act = require('jest-react').act; Suspense = React.Suspense; + getCacheSignal = React.unstable_getCacheSignal; getCacheForType = React.unstable_getCacheForType; useCacheRefresh = React.unstable_useCacheRefresh; startTransition = React.startTransition; @@ -52,6 +54,7 @@ describe('ReactCache', () => { const newRecord = { status: 'resolved', value: text, + cleanupScheduled: false, }; data.set(text, newRecord); } else if (record.status === 'pending') { @@ -64,6 +67,7 @@ describe('ReactCache', () => { const newRecord = { status: 'rejected', value: error, + cleanupScheduled: false, }; data.set(text, newRecord); } else if (record.status === 'pending') { @@ -76,9 +80,21 @@ describe('ReactCache', () => { } function readText(text) { + const signal = getCacheSignal(); const textCache = getCacheForType(createTextCache); const record = textCache.data.get(text); if (record !== undefined) { + if (!record.cleanupScheduled) { + // This record was seeded prior to the abort signal being available: + // schedule a cleanup function for it. + // TODO: Add ability to cleanup entries seeded w useCacheRefresh() + record.cleanupScheduled = true; + signal.addEventListener('abort', () => { + Scheduler.unstable_yieldValue( + `Cache cleanup: ${text} [v${textCache.version}]`, + ); + }); + } switch (record.status) { case 'pending': throw record.value; @@ -115,9 +131,15 @@ describe('ReactCache', () => { const newRecord = { status: 'pending', value: thenable, + cleanupScheduled: true, }; textCache.data.set(text, newRecord); + signal.addEventListener('abort', () => { + Scheduler.unstable_yieldValue( + `Cache cleanup: ${text} [v${textCache.version}]`, + ); + }); throw thenable; } } @@ -180,6 +202,13 @@ describe('ReactCache', () => { }); expect(Scheduler).toHaveYielded(['A']); expect(root).toMatchRenderedOutput('A'); + + await act(async () => { + root.render('Bye'); + }); + // no cleanup: cache is still retained at the root + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('Bye'); }); // @gate experimental || www @@ -200,12 +229,19 @@ describe('ReactCache', () => { }); expect(Scheduler).toHaveYielded(['A']); expect(root).toMatchRenderedOutput('A'); + + await act(async () => { + root.render('Bye'); + }); + // no cleanup: cache is still retained at the root + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('Bye'); }); // @gate experimental || www test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => { - function App({text}) { - return ( + function App({showMore}) { + return showMore ? ( <> }> @@ -218,6 +254,8 @@ describe('ReactCache', () => { + ) : ( + '(empty)' ); } @@ -225,6 +263,12 @@ describe('ReactCache', () => { await act(async () => { root.render(); }); + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('(empty)'); + + await act(async () => { + root.render(); + }); // Even though there are two new trees, they should share the same // data cache. So there should be only a single cache miss for A. expect(Scheduler).toHaveYielded([ @@ -239,6 +283,15 @@ describe('ReactCache', () => { }); expect(Scheduler).toHaveYielded(['A', 'A']); expect(root).toMatchRenderedOutput('AA'); + + await act(async () => { + root.render('Bye'); + }); + // cleanup occurs for the cache shared by the inner cache boundaries (which + // are not shared w the root because they were added in an update) + // note that no cache is created for the root since the cache is never accessed + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v1]']); + expect(root).toMatchRenderedOutput('Bye'); }); // @gate experimental || www @@ -261,8 +314,8 @@ describe('ReactCache', () => { await act(async () => { root.render(); }); - // Even though there are two new trees, they should share the same - // data cache. So there should be only a single cache miss for A. + // Even though there is a nested boundary, it should share the same + // data cache as the root. So there should be only a single cache miss for A. expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']); expect(root).toMatchRenderedOutput('Loading...'); @@ -271,6 +324,13 @@ describe('ReactCache', () => { }); expect(Scheduler).toHaveYielded(['A', 'A']); expect(root).toMatchRenderedOutput('AA'); + + await act(async () => { + root.render('Bye'); + }); + // no cleanup: cache is still retained at the root + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('Bye'); }, ); @@ -309,6 +369,13 @@ describe('ReactCache', () => { 'A [v1]', ]); expect(root).toMatchRenderedOutput('A [v1]A [v1]'); + + await act(async () => { + root.render('Bye'); + }); + // no cleanup: cache is still retained at the root + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('Bye'); }); // @gate experimental || www @@ -356,10 +423,21 @@ describe('ReactCache', () => { }); expect(Scheduler).toHaveYielded(['A [v2]']); expect(root).toMatchRenderedOutput('A [v1]A [v2]'); + + // Replace all the children: this should retain the root Cache instance, + // but cleanup the separate cache instance created for the fresh cache + // boundary + await act(async () => { + root.render('Bye!'); + }); + // Cleanup occurs for the *second* cache instance: the first is still + // referenced by the root + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']); + expect(root).toMatchRenderedOutput('Bye!'); }); // @gate experimental || www - test('inner content uses same cache as shell if spawned by the same transition', async () => { + test('inner/outer cache boundaries uses the same cache instance on initial render', async () => { const root = ReactNoop.createRoot(); function App() { @@ -431,10 +509,109 @@ describe('ReactCache', () => {
Content
, ); + + await act(async () => { + root.render('Bye'); + }); + // no cleanup: cache is still retained at the root + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('Bye'); + }); + + // @gate experimental || www + test('inner/ outer cache boundaries added in the same update use the same cache instance', async () => { + const root = ReactNoop.createRoot(); + + function App({showMore}) { + return showMore ? ( + + }> + {/* The shell reads A */} + + {/* The inner content reads both A and B */} + }> + + + + + + + + ) : ( + '(empty)' + ); + } + + function Shell({children}) { + readText('A'); + return ( + <> +
+ +
+
{children}
+ + ); + } + + function Content() { + readText('A'); + readText('B'); + return ; + } + + await act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('(empty)'); + + await act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading shell...']); + expect(root).toMatchRenderedOutput('Loading shell...'); + + await act(async () => { + resolveMostRecentTextCache('A'); + }); + expect(Scheduler).toHaveYielded([ + 'Shell', + // There's a cache miss for B, because it hasn't been read yet. But not + // A, because it was cached when we rendered the shell. + 'Cache miss! [B]', + 'Loading content...', + ]); + expect(root).toMatchRenderedOutput( + <> +
Shell
+
Loading content...
+ , + ); + + await act(async () => { + resolveMostRecentTextCache('B'); + }); + expect(Scheduler).toHaveYielded(['Content']); + expect(root).toMatchRenderedOutput( + <> +
Shell
+
Content
+ , + ); + + await act(async () => { + root.render('Bye'); + }); + expect(Scheduler).toHaveYielded([ + 'Cache cleanup: A [v1]', + 'Cache cleanup: B [v1]', + ]); + expect(root).toMatchRenderedOutput('Bye'); }); // @gate experimental || www - test('refresh a cache', async () => { + test('refresh a cache boundary', async () => { let refresh; function App() { refresh = useCacheRefresh(); @@ -474,6 +651,14 @@ describe('ReactCache', () => { // Note that the version has updated expect(Scheduler).toHaveYielded(['A [v2]']); expect(root).toMatchRenderedOutput('A [v2]'); + + await act(async () => { + root.render('Bye'); + }); + // the original cache instance does not cleanup since it is still referenced + // by the root, but the refreshed inner cache does cleanup + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']); + expect(root).toMatchRenderedOutput('Bye'); }); // @gate experimental || www @@ -512,9 +697,64 @@ describe('ReactCache', () => { await act(async () => { resolveMostRecentTextCache('A'); }); - // Note that the version has updated - expect(Scheduler).toHaveYielded(['A [v2]']); + // Note that the version has updated, and the previous cache is cleared + expect(Scheduler).toHaveYielded(['A [v2]', 'Cache cleanup: A [v1]']); expect(root).toMatchRenderedOutput('A [v2]'); + + await act(async () => { + root.render('Bye'); + }); + // the original root cache already cleaned up when the refresh completed + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('Bye'); + }); + + // @gate experimental || www + test('refresh the root cache without a transition', async () => { + let refresh; + function App() { + refresh = useCacheRefresh(); + return ; + } + + // Mount initial data + const root = ReactNoop.createRoot(); + await act(async () => { + root.render( + }> + + , + ); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']); + expect(root).toMatchRenderedOutput('Loading...'); + + await act(async () => { + resolveMostRecentTextCache('A'); + }); + expect(Scheduler).toHaveYielded(['A [v1]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + // Refresh for new data. + await act(async () => { + refresh(); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']); + expect(root).toMatchRenderedOutput('Loading...'); + + await act(async () => { + resolveMostRecentTextCache('A'); + }); + // Note that the version has updated, and the previous cache is cleared + expect(Scheduler).toHaveYielded(['A [v2]', 'Cache cleanup: A [v1]']); + expect(root).toMatchRenderedOutput('A [v2]'); + + await act(async () => { + root.render('Bye'); + }); + // the original root cache already cleaned up when the refresh completed + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('Bye'); }); // @gate experimental || www @@ -556,8 +796,16 @@ describe('ReactCache', () => { startTransition(() => refresh(createTextCache, cache)); }); // The root should re-render without a cache miss. + // The cache is not cleared up yet, since it's still reference by the root expect(Scheduler).toHaveYielded(['A [v2]']); expect(root).toMatchRenderedOutput('A [v2]'); + + await act(async () => { + root.render('Bye'); + }); + // the refreshed cache boundary is unmounted and cleans up + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']); + expect(root).toMatchRenderedOutput('Bye'); }); // @gate experimental || www @@ -621,8 +869,22 @@ describe('ReactCache', () => { await act(async () => { resolveMostRecentTextCache('A'); }); - expect(Scheduler).toHaveYielded(['A [v3]', 'A [v3]']); + expect(Scheduler).toHaveYielded([ + 'A [v3]', + 'A [v3]', + // once the refresh completes the inner showMore boundary frees its previous + // cache instance, since it is now using the refreshed parent instance. + 'Cache cleanup: A [v2]', + ]); expect(root).toMatchRenderedOutput('A [v3]A [v3]'); + + await act(async () => { + root.render('Bye!'); + }); + // Unmounting children releases the refreshed cache instance only; the root + // still retains the original cache instance used for the first render + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v3]']); + expect(root).toMatchRenderedOutput('Bye!'); }); // @gate experimental || www @@ -695,6 +957,21 @@ describe('ReactCache', () => { }); expect(Scheduler).toHaveYielded(['A [v2]']); expect(root).toMatchRenderedOutput('A [v2]A [v1]'); + + // Unmount children: this should clear *both* cache instances: + // the root doesn't have a cache instance (since it wasn't accessed + // during the initial render, and all subsequent cache accesses were within + // a fresh boundary). Therefore this causes cleanup for both the fresh cache + // instance in the refreshed first boundary and cleanup for the non-refreshed + // sibling boundary. + await act(async () => { + root.render('Bye!'); + }); + expect(Scheduler).toHaveYielded([ + 'Cache cleanup: A [v2]', + 'Cache cleanup: A [v1]', + ]); + expect(root).toMatchRenderedOutput('Bye!'); }, ); @@ -733,6 +1010,7 @@ describe('ReactCache', () => { 'Cache miss! [B]', 'Loading...', ]); + expect(root).toMatchRenderedOutput('Loading...'); await act(async () => { // This will resolve the content in the first cache @@ -750,6 +1028,7 @@ describe('ReactCache', () => { 'A [v1]', 'B [v1]', ]); + expect(root).toMatchRenderedOutput('Loading... A [v1] B [v1]'); // Now resolve the second tree await act(async () => { @@ -757,6 +1036,15 @@ describe('ReactCache', () => { }); expect(Scheduler).toHaveYielded(['A [v2]']); expect(root).toMatchRenderedOutput('A [v2] A [v1] B [v1]'); + + await act(async () => { + root.render('Bye!'); + }); + // Unmounting children releases both cache boundaries, but the original + // cache instance (used by second boundary) is still referenced by the root. + // only the second cache instance is freed. + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']); + expect(root).toMatchRenderedOutput('Bye!'); }, ); @@ -841,6 +1129,19 @@ describe('ReactCache', () => { }); expect(Scheduler).toHaveYielded(['A [v1]', 'A [v1]', 'A [v2]']); expect(root).toMatchRenderedOutput('A [v1]A [v1]A [v2]'); + + // Unmount children: the first text cache instance is created only after the root + // commits, so both fresh cache instances are released by their cache boundaries, + // cleaning up v1 (used for the first two children which render togeether) and + // v2 (used for the third boundary added later). + await act(async () => { + root.render('Bye!'); + }); + expect(Scheduler).toHaveYielded([ + 'Cache cleanup: A [v1]', + 'Cache cleanup: A [v2]', + ]); + expect(root).toMatchRenderedOutput('Bye!'); }); // @gate experimental || www @@ -863,7 +1164,7 @@ describe('ReactCache', () => { }> {shouldShow ? ( - + ) : null} @@ -880,7 +1181,7 @@ describe('ReactCache', () => { const root = ReactNoop.createRoot(); await act(async () => { - root.render(); + root.render(); }); expect(Scheduler).toHaveYielded(['0']); expect(root).toMatchRenderedOutput('0'); @@ -908,7 +1209,331 @@ describe('ReactCache', () => { await act(async () => { resolveMostRecentTextCache('A'); }); - expect(Scheduler).toHaveYielded(['A']); - expect(root).toMatchRenderedOutput('A1'); + expect(Scheduler).toHaveYielded(['A [v1]']); + expect(root).toMatchRenderedOutput('A [v1]1'); + + // Unmount children: the first text cache instance is created only after initial + // render after calling showMore(). This instance is cleaned up when that boundary + // is unmounted. Bc root cache instance is never accessed, the inner cache + // boundary ends up at v1. + await act(async () => { + root.render('Bye!'); + }); + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v1]']); + expect(root).toMatchRenderedOutput('Bye!'); + }); + + // @gate experimental || www + test('cache boundary uses a fresh cache when its key changes', async () => { + const root = ReactNoop.createRoot(); + seedNextTextCache('A'); + await act(async () => { + root.render( + + + + + , + ); + }); + expect(Scheduler).toHaveYielded(['A [v1]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + seedNextTextCache('B'); + await act(async () => { + root.render( + + + + + , + ); + }); + expect(Scheduler).toHaveYielded(['B [v2]']); + expect(root).toMatchRenderedOutput('B [v2]'); + + // Unmount children: the fresh cache instance for B cleans up since the cache boundary + // is the only owner, while the original cache instance (for A) is still retained by + // the root. + await act(async () => { + root.render('Bye!'); + }); + expect(Scheduler).toHaveYielded(['Cache cleanup: B [v2]']); + expect(root).toMatchRenderedOutput('Bye!'); + }); + + // @gate experimental || www + test('overlapping transitions after an initial mount use the same fresh cache', async () => { + const root = ReactNoop.createRoot(); + await act(async () => { + root.render( + + + + + , + ); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [A]']); + expect(root).toMatchRenderedOutput('Loading...'); + + await act(async () => { + resolveMostRecentTextCache('A'); + }); + expect(Scheduler).toHaveYielded(['A [v1]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + // After a mount, subsequent transitions use a fresh cache + await act(async () => { + startTransition(() => { + root.render( + + + + + , + ); + }); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [B]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + // Update to a different text and with a different key for the cache + // boundary: this should still use the fresh cache instance created + // for the earlier transition + await act(async () => { + startTransition(() => { + root.render( + + + + + , + ); + }); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [C]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + await act(async () => { + resolveMostRecentTextCache('C'); + }); + expect(Scheduler).toHaveYielded(['C [v2]']); + expect(root).toMatchRenderedOutput('C [v2]'); + + // Unmount children: the fresh cache used for the updates is freed, while the + // original cache (with A) is still retained at the root. + await act(async () => { + root.render('Bye!'); + }); + expect(Scheduler).toHaveYielded([ + 'Cache cleanup: B [v2]', + 'Cache cleanup: C [v2]', + ]); + expect(root).toMatchRenderedOutput('Bye!'); + }); + + // @gate experimental || www + test('overlapping updates after an initial mount use the same fresh cache', async () => { + const root = ReactNoop.createRoot(); + await act(async () => { + root.render( + + + + + , + ); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [A]']); + expect(root).toMatchRenderedOutput('Loading...'); + + await act(async () => { + resolveMostRecentTextCache('A'); + }); + expect(Scheduler).toHaveYielded(['A [v1]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + // After a mount, subsequent updates use a fresh cache + await act(async () => { + root.render( + + + + + , + ); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [B]']); + expect(root).toMatchRenderedOutput('Loading...'); + + // A second update uses the same fresh cache: even though this is a new + // Cache boundary, the render uses the fresh cache from the pending update. + await act(async () => { + root.render( + + + + + , + ); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [C]']); + expect(root).toMatchRenderedOutput('Loading...'); + + await act(async () => { + resolveMostRecentTextCache('C'); + }); + expect(Scheduler).toHaveYielded(['C [v2]']); + expect(root).toMatchRenderedOutput('C [v2]'); + + // Unmount children: the fresh cache used for the updates is freed, while the + // original cache (with A) is still retained at the root. + await act(async () => { + root.render('Bye!'); + }); + expect(Scheduler).toHaveYielded([ + 'Cache cleanup: B [v2]', + 'Cache cleanup: C [v2]', + ]); + expect(root).toMatchRenderedOutput('Bye!'); + }); + + // @gate experimental || www + test('cleans up cache only used in an aborted transition', async () => { + const root = ReactNoop.createRoot(); + seedNextTextCache('A'); + await act(async () => { + root.render( + + + + + , + ); + }); + expect(Scheduler).toHaveYielded(['A [v1]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + // Start a transition from A -> B..., which should create a fresh cache + // for the new cache boundary (bc of the different key) + await act(async () => { + startTransition(() => { + root.render( + + + + + , + ); + }); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [B]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + // ...but cancel by transitioning "back" to A (which we never really left) + await act(async () => { + startTransition(() => { + root.render( + + + + + , + ); + }); + }); + expect(Scheduler).toHaveYielded(['A [v1]', 'Cache cleanup: B [v2]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + // Unmount children: ... + await act(async () => { + root.render('Bye!'); + }); + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('Bye!'); + }); + + // @gate experimental || www + test.skip('if a root cache refresh never commits its fresh cache is released', async () => { + const root = ReactNoop.createRoot(); + let refresh; + function Example({text}) { + refresh = useCacheRefresh(); + return ; + } + seedNextTextCache('A'); + await act(async () => { + root.render( + + + , + ); + }); + expect(Scheduler).toHaveYielded(['A [v1]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + await act(async () => { + startTransition(() => { + refresh(); + }); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [A]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + await act(async () => { + root.render('Bye!'); + }); + expect(Scheduler).toHaveYielded([ + // TODO: the v1 cache should *not* be cleaned up, it is still retained by the root + // The following line is presently yielded but should not be: + // 'Cache cleanup: A [v1]', + + // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh + // The following line is presently not yielded but should be: + 'Cache cleanup: A [v2]', + ]); + expect(root).toMatchRenderedOutput('Bye!'); + }); + + // @gate experimental || www + test.skip('if a cache boundary refresh never commits its fresh cache is released', async () => { + const root = ReactNoop.createRoot(); + let refresh; + function Example({text}) { + refresh = useCacheRefresh(); + return ; + } + seedNextTextCache('A'); + await act(async () => { + root.render( + + + + + , + ); + }); + expect(Scheduler).toHaveYielded(['A [v1]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + await act(async () => { + startTransition(() => { + refresh(); + }); + }); + expect(Scheduler).toHaveYielded(['Cache miss! [A]']); + expect(root).toMatchRenderedOutput('A [v1]'); + + // Unmount the boundary before the refresh can complete + await act(async () => { + root.render('Bye!'); + }); + expect(Scheduler).toHaveYielded([ + // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh + // The following line is presently not yielded but should be: + 'Cache cleanup: A [v2]', + ]); + expect(root).toMatchRenderedOutput('Bye!'); }); }); diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js index 9b6ec83a40d39..6b1e4290d0102 100644 --- a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js +++ b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js @@ -409,30 +409,30 @@ describe('SchedulingProfiler', () => { if (gate(flags => flags.enableSchedulingProfiler)) { expect(getMarks()).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--schedule-state-update-1-Example", - "--layout-effects-stop", - "--render-start-1", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-1", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - "--commit-stop", - ] - `); + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--schedule-state-update-1-Example", + "--layout-effects-stop", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + "--commit-stop", + ] + `); } }); @@ -462,30 +462,30 @@ describe('SchedulingProfiler', () => { if (gate(flags => flags.enableSchedulingProfiler)) { expect(getMarks()).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--schedule-forced-update-1-Example", - "--layout-effects-stop", - "--render-start-1", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-1", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - "--commit-stop", - ] - `); + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--schedule-forced-update-1-Example", + "--layout-effects-stop", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + "--commit-stop", + ] + `); } }); @@ -701,22 +701,22 @@ describe('SchedulingProfiler', () => { if (gate(flags => flags.enableSchedulingProfiler)) { expect(getMarks()).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - "--render-start-16", - "--component-render-start-Example", - "--schedule-state-update-16-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--layout-effects-stop", - "--commit-stop", - ] - `); + Array [ + "--schedule-render-16", + "--render-start-16", + "--component-render-start-Example", + "--schedule-state-update-16-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); } }); @@ -746,35 +746,35 @@ describe('SchedulingProfiler', () => { if (gate(flags => flags.enableSchedulingProfiler)) { expect(getMarks()).toMatchInlineSnapshot(` - Array [ - "--schedule-render-1", - "--render-start-1", - "--component-render-start-ErrorBoundary", - "--component-render-stop", - "--component-render-start-ExampleThatThrows", - "--component-render-start-ExampleThatThrows", - "--component-render-stop", - "--error-ExampleThatThrows-mount-Expected error", - "--render-stop", - "--commit-start-1", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-1", - "--schedule-state-update-1-ErrorBoundary", - "--layout-effects-stop", - "--commit-stop", - "--render-start-1", - "--component-render-start-ErrorBoundary", - "--component-render-stop", - "--render-stop", - "--commit-start-1", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - ] - `); + Array [ + "--schedule-render-1", + "--render-start-1", + "--component-render-start-ErrorBoundary", + "--component-render-stop", + "--component-render-start-ExampleThatThrows", + "--component-render-start-ExampleThatThrows", + "--component-render-stop", + "--error-ExampleThatThrows-mount-Expected error", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--schedule-state-update-1-ErrorBoundary", + "--layout-effects-stop", + "--commit-stop", + "--render-start-1", + "--component-render-start-ErrorBoundary", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + ] + `); } }); diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js index 13ca3988b2a71..a27c0271c116a 100644 --- a/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js +++ b/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js @@ -89,20 +89,20 @@ describe('SchedulingProfiler labels', () => { if (gate(flags => flags.enableSchedulingProfiler)) { expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "__v3", - "--schedule-render-1", - "--render-start-1", - "--render-stop", - "--commit-start-1", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-1", - "--layout-effects-stop", - "--commit-stop", - ] - `); + Array [ + "__v3", + "--schedule-render-1", + "--render-start-1", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); } }); @@ -114,11 +114,11 @@ describe('SchedulingProfiler labels', () => { root.render(
); expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "__v3", - "--schedule-render-16", - ] - `); + Array [ + "__v3", + "--schedule-render-16", + ] + `); }); } }); @@ -152,21 +152,21 @@ describe('SchedulingProfiler labels', () => { }); expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-state-update-1-App", - "--render-start-1", - "--component-render-start-App", - "--component-render-stop", - "--render-stop", - "--commit-start-1", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-1", - "--layout-effects-stop", - "--commit-stop", - ] - `); + Array [ + "--schedule-state-update-1-App", + "--render-start-1", + "--component-render-start-App", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); } }); @@ -196,21 +196,21 @@ describe('SchedulingProfiler labels', () => { }); expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-state-update-4-App", - "--render-start-4", - "--component-render-start-App", - "--component-render-stop", - "--render-stop", - "--commit-start-4", - "--react-version-17.0.3", - "--profiler-version-1", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-4", - "--layout-effects-stop", - "--commit-stop", - ] - `); + Array [ + "--schedule-state-update-4-App", + "--render-start-4", + "--component-render-start-App", + "--component-render-stop", + "--render-stop", + "--commit-start-4", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-4", + "--layout-effects-stop", + "--commit-stop", + ] + `); } }); }); diff --git a/packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js b/packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js index e443aa0a23190..f2041b4ba1e32 100644 --- a/packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js +++ b/packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js @@ -113,6 +113,7 @@ describe('StrictEffectsMode defaults', () => { , ); + expect(Scheduler).toHaveYielded([]); expect(Scheduler).toFlushUntilNextPaint([ // Cleanup and re-run "one" (and "two") since there is no dependencies array. 'useLayoutEffect unmount "one"', diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 5335c4053a70e..568a8ada613d9 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -37,6 +37,7 @@ export { unstable_LegacyHidden, unstable_Offscreen, unstable_Scope, + unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, unstable_useOpaqueIdentifier, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index 19490fb214c97..7491bbb7e832d 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -33,6 +33,7 @@ export { unstable_DebugTracingMode, unstable_LegacyHidden, unstable_Offscreen, + unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, unstable_useOpaqueIdentifier, diff --git a/packages/react/index.js b/packages/react/index.js index b4a3bf6e11b81..59cc05f0254e6 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -58,6 +58,7 @@ export { unstable_LegacyHidden, unstable_Offscreen, unstable_Scope, + unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, unstable_useOpaqueIdentifier, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 2b847d2336312..cd60ee426fa65 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -36,6 +36,7 @@ export { unstable_LegacyHidden, unstable_Offscreen, unstable_Scope, + unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, unstable_useOpaqueIdentifier, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 2246d32db662e..d29858c9b07fd 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -35,6 +35,7 @@ import {lazy} from './ReactLazy'; import {forwardRef} from './ReactForwardRef'; import {memo} from './ReactMemo'; import { + getCacheSignal, getCacheForType, useCallback, useContext, @@ -119,6 +120,7 @@ export { REACT_SUSPENSE_LIST_TYPE as SuspenseList, REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden, REACT_OFFSCREEN_TYPE as unstable_Offscreen, + getCacheSignal as unstable_getCacheSignal, getCacheForType as unstable_getCacheForType, useCacheRefresh as unstable_useCacheRefresh, REACT_CACHE_TYPE as unstable_Cache, diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 0108c545fae5f..1892f926c59cf 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -41,6 +41,12 @@ function resolveDispatcher() { return ((dispatcher: any): Dispatcher); } +export function getCacheSignal(): AbortSignal { + const dispatcher = resolveDispatcher(); + // $FlowFixMe This is unstable, thus optional + return dispatcher.getCacheSignal(); +} + export function getCacheForType(resourceType: () => T): T { const dispatcher = resolveDispatcher(); // $FlowFixMe This is unstable, thus optional diff --git a/packages/react/unstable-shared-subset.experimental.js b/packages/react/unstable-shared-subset.experimental.js index 9381778b4435d..a663ca8a5a89d 100644 --- a/packages/react/unstable-shared-subset.experimental.js +++ b/packages/react/unstable-shared-subset.experimental.js @@ -25,6 +25,7 @@ export { memo, startTransition, unstable_DebugTracingMode, + unstable_getCacheSignal, unstable_getCacheForType, unstable_useOpaqueIdentifier, useCallback, diff --git a/scripts/jest/setupEnvironment.js b/scripts/jest/setupEnvironment.js index 2ba88b156169d..d2d510088c45e 100644 --- a/scripts/jest/setupEnvironment.js +++ b/scripts/jest/setupEnvironment.js @@ -1,5 +1,7 @@ /* eslint-disable */ +const AbortController = require('abort-controller'); + const NODE_ENV = process.env.NODE_ENV; if (NODE_ENV !== 'development' && NODE_ENV !== 'production') { throw new Error('NODE_ENV must either be set to development or production.'); @@ -21,6 +23,8 @@ global.__EXPERIMENTAL__ = global.__VARIANT__ = !!process.env.VARIANT; +global.AbortController = AbortController; + if (typeof window !== 'undefined') { global.requestIdleCallback = function(callback) { return setTimeout(() => { From d5b6b4b865ebf13a1eaf2342d623101056e5e197 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 21 Oct 2021 18:31:26 -0400 Subject: [PATCH 060/109] Expand act warning to cover all APIs that might schedule React work (#22607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move isActEnvironment check to function that warns I'm about to fork the behavior in legacy roots versus concurrent roots even further, so I'm lifting this up so I only have to fork once. * Lift `mode` check, too Similar to previous commit. I only want to check this once. Not for performance reasons, but so the logic is easier to follow. * Expand act warning to include non-hook APIs In a test environment, React warns if an update isn't wrapped with act — but only if the update originates from a hook API, like useState. We did it this way for backwards compatibility with tests that were written before the act API was introduced. Those tests didn't require act, anyway, because in a legacy root, all tasks are synchronous except for `useEffect`. However, in a concurrent root, nearly every task is asynchronous. Even tasks that are synchronous may spawn additional asynchronous work. So all updates need to be wrapped with act, regardless of whether they originate from a hook, a class, a root, or any other type of component. This commit expands the act warning to include any API that triggers an update. It does not currently account for renders that are caused by a Suspense promise resolving; those are modelled slightly differently from updates. I'll fix that in the next step. I also removed the check for whether an update is batched. It shouldn't matter, because even a batched update can spawn asynchronous work, which needs to be flushed by act. This change only affects concurrent roots. The behavior in legacy roots is the same. * Expand act warning to include Suspense resolutions For the same reason we warn when an update is not wrapped with act, we should warn if a Suspense promise resolution is not wrapped with act. Both "pings" and "retries". Legacy root behavior is unchanged. --- .../ReactDevToolsHooksIntegration-test.js | 28 ++- .../__tests__/preprocessData-test.internal.js | 12 +- .../src/__tests__/ReactTestUtilsAct-test.js | 38 ++- .../react-reconciler/src/ReactFiberAct.new.js | 54 ++-- .../react-reconciler/src/ReactFiberAct.old.js | 54 ++-- .../src/ReactFiberHooks.new.js | 21 +- .../src/ReactFiberHooks.old.js | 21 +- .../src/ReactFiberWorkLoop.new.js | 71 +++++- .../src/ReactFiberWorkLoop.old.js | 71 +++++- .../__tests__/DebugTracing-test.internal.js | 203 +++++++-------- .../src/__tests__/ReactActWarnings-test.js | 237 +++++++++++++++++- .../ReactFiberHostContext-test.internal.js | 2 + .../src/__tests__/ReactIsomorphicAct-test.js | 6 + 13 files changed, 578 insertions(+), 240 deletions(-) diff --git a/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js index 3eea121d65b36..a64a8d794ad94 100644 --- a/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js @@ -20,6 +20,8 @@ describe('React hooks DevTools integration', () => { let scheduleUpdate; let setSuspenseHandler; + global.IS_REACT_ACT_ENVIRONMENT = true; + beforeEach(() => { global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { inject: injected => { @@ -64,7 +66,7 @@ describe('React hooks DevTools integration', () => { expect(stateHook.isStateEditable).toBe(true); if (__DEV__) { - overrideHookState(fiber, stateHook.id, [], 10); + act(() => overrideHookState(fiber, stateHook.id, [], 10)); expect(renderer.toJSON()).toEqual({ type: 'div', props: {}, @@ -116,7 +118,7 @@ describe('React hooks DevTools integration', () => { expect(reducerHook.isStateEditable).toBe(true); if (__DEV__) { - overrideHookState(fiber, reducerHook.id, ['foo'], 'def'); + act(() => overrideHookState(fiber, reducerHook.id, ['foo'], 'def')); expect(renderer.toJSON()).toEqual({ type: 'div', props: {}, @@ -164,13 +166,12 @@ describe('React hooks DevTools integration', () => { expect(stateHook.isStateEditable).toBe(true); if (__DEV__) { - overrideHookState(fiber, stateHook.id, ['count'], 10); + act(() => overrideHookState(fiber, stateHook.id, ['count'], 10)); expect(renderer.toJSON()).toEqual({ type: 'div', props: {}, children: ['count:', '10'], }); - act(() => setStateFn(state => ({count: state.count + 1}))); expect(renderer.toJSON()).toEqual({ type: 'div', @@ -233,7 +234,8 @@ describe('React hooks DevTools integration', () => { } }); - it('should support overriding suspense in concurrent mode', () => { + // @gate __DEV__ + it('should support overriding suspense in concurrent mode', async () => { if (__DEV__) { // Lock the first render setSuspenseHandler(() => true); @@ -243,13 +245,15 @@ describe('React hooks DevTools integration', () => { return 'Done'; } - const renderer = ReactTestRenderer.create( -
- - - -
, - {unstable_isConcurrent: true}, + const renderer = await act(() => + ReactTestRenderer.create( +
+ + + +
, + {unstable_isConcurrent: true}, + ), ); expect(Scheduler).toFlushAndYield([]); diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js index 94ada274a770b..758afc1e15f2c 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js @@ -17,8 +17,6 @@ import { } from '../../constants'; import REACT_VERSION from 'shared/ReactVersion'; -global.IS_REACT_ACT_ENVIRONMENT = true; - describe('getLanesFromTransportDecimalBitmask', () => { it('should return array of lane numbers from bitmask string', () => { expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]); @@ -210,6 +208,8 @@ describe('preprocessData', () => { tid = 0; pid = 0; startTime = 0; + + global.IS_REACT_ACT_ENVIRONMENT = true; }); afterEach(() => { @@ -1251,7 +1251,7 @@ describe('preprocessData', () => { testMarks.push(...createUserTimingData(clearedMarks)); - const data = await preprocessData(testMarks); + const data = await act(() => preprocessData(testMarks)); expect(data.suspenseEvents).toHaveLength(1); expect(data.suspenseEvents[0].promiseName).toBe('Testing displayName'); } @@ -1367,6 +1367,8 @@ describe('preprocessData', () => { const root = ReactDOM.createRoot(document.createElement('div')); + // Temporarily turn off the act environment, since we're intentionally using Scheduler instead. + global.IS_REACT_ACT_ENVIRONMENT = false; React.startTransition(() => { // Start rendering an async update (but don't finish). root.render( @@ -1837,7 +1839,7 @@ describe('preprocessData', () => { testMarks.push(...createUserTimingData(clearedMarks)); - const data = await preprocessData(testMarks); + const data = await act(() => preprocessData(testMarks)); expect(data.suspenseEvents).toHaveLength(1); expect(data.suspenseEvents[0].warning).toMatchInlineSnapshot( `"A component suspended during an update which caused a fallback to be shown. Consider using the Transition API to avoid hiding components after they've been mounted."`, @@ -1895,7 +1897,7 @@ describe('preprocessData', () => { testMarks.push(...createUserTimingData(clearedMarks)); - const data = await preprocessData(testMarks); + const data = await act(() => preprocessData(testMarks)); expect(data.suspenseEvents).toHaveLength(1); expect(data.suspenseEvents[0].warning).toBe(null); } diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js index d0992dce53d41..556772500ee2f 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js @@ -32,18 +32,31 @@ describe('ReactTestUtils.act()', () => { let concurrentRoot = null; const renderConcurrent = (el, dom) => { concurrentRoot = ReactDOM.createRoot(dom); - concurrentRoot.render(el); + if (__DEV__) { + act(() => concurrentRoot.render(el)); + } else { + concurrentRoot.render(el); + } }; const unmountConcurrent = _dom => { - if (concurrentRoot !== null) { - concurrentRoot.unmount(); - concurrentRoot = null; + if (__DEV__) { + act(() => { + if (concurrentRoot !== null) { + concurrentRoot.unmount(); + concurrentRoot = null; + } + }); + } else { + if (concurrentRoot !== null) { + concurrentRoot.unmount(); + concurrentRoot = null; + } } }; const rerenderConcurrent = el => { - concurrentRoot.render(el); + act(() => concurrentRoot.render(el)); }; runActTests( @@ -98,22 +111,29 @@ describe('ReactTestUtils.act()', () => { ]); }); + // @gate __DEV__ it('does not warn in concurrent mode', () => { const root = ReactDOM.createRoot(document.createElement('div')); - root.render(); + act(() => root.render()); Scheduler.unstable_flushAll(); }); it('warns in concurrent mode if root is strict', () => { + // TODO: We don't need this error anymore in concurrent mode because + // effects can only be scheduled as the result of an update, and we now + // enforce all updates must be wrapped with act, not just hook updates. expect(() => { const root = ReactDOM.createRoot(document.createElement('div'), { unstable_strictMode: true, }); root.render(); - Scheduler.unstable_flushAll(); - }).toErrorDev([ + }).toErrorDev( + 'An update to Root inside a test was not wrapped in act(...)', + {withoutStack: true}, + ); + expect(() => Scheduler.unstable_flushAll()).toErrorDev( 'An update to App ran an effect, but was not wrapped in act(...)', - ]); + ); }); }); }); diff --git a/packages/react-reconciler/src/ReactFiberAct.new.js b/packages/react-reconciler/src/ReactFiberAct.new.js index 18055e7c738c7..27102b5e92531 100644 --- a/packages/react-reconciler/src/ReactFiberAct.new.js +++ b/packages/react-reconciler/src/ReactFiberAct.new.js @@ -12,11 +12,32 @@ import type {Fiber} from './ReactFiber.new'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import {warnsIfNotActing} from './ReactFiberHostConfig'; -import {ConcurrentMode} from './ReactTypeOfMode'; const {ReactCurrentActQueue} = ReactSharedInternals; -export function isActEnvironment(fiber: Fiber) { +export function isLegacyActEnvironment(fiber: Fiber) { + if (__DEV__) { + // Legacy mode. We preserve the behavior of React 17's act. It assumes an + // act environment whenever `jest` is defined, but you can still turn off + // spurious warnings by setting IS_REACT_ACT_ENVIRONMENT explicitly + // to false. + + const isReactActEnvironmentGlobal = + // $FlowExpectedError – Flow doesn't know about IS_REACT_ACT_ENVIRONMENT global + typeof IS_REACT_ACT_ENVIRONMENT !== 'undefined' + ? IS_REACT_ACT_ENVIRONMENT + : undefined; + + // $FlowExpectedError - Flow doesn't know about jest + const jestIsDefined = typeof jest !== 'undefined'; + return ( + warnsIfNotActing && jestIsDefined && isReactActEnvironmentGlobal !== false + ); + } + return false; +} + +export function isConcurrentActEnvironment() { if (__DEV__) { const isReactActEnvironmentGlobal = // $FlowExpectedError – Flow doesn't know about IS_REACT_ACT_ENVIRONMENT global @@ -24,31 +45,14 @@ export function isActEnvironment(fiber: Fiber) { ? IS_REACT_ACT_ENVIRONMENT : undefined; - if (fiber.mode & ConcurrentMode) { - if ( - !isReactActEnvironmentGlobal && - ReactCurrentActQueue.current !== null - ) { - // TODO: Include link to relevant documentation page. - console.error( - 'The current testing environment is not configured to support ' + - 'act(...)', - ); - } - return isReactActEnvironmentGlobal; - } else { - // Legacy mode. We preserve the behavior of React 17's act. It assumes an - // act environment whenever `jest` is defined, but you can still turn off - // spurious warnings by setting IS_REACT_ACT_ENVIRONMENT explicitly - // to false. - // $FlowExpectedError - Flow doesn't know about jest - const jestIsDefined = typeof jest !== 'undefined'; - return ( - warnsIfNotActing && - jestIsDefined && - isReactActEnvironmentGlobal !== false + if (!isReactActEnvironmentGlobal && ReactCurrentActQueue.current !== null) { + // TODO: Include link to relevant documentation page. + console.error( + 'The current testing environment is not configured to support ' + + 'act(...)', ); } + return isReactActEnvironmentGlobal; } return false; } diff --git a/packages/react-reconciler/src/ReactFiberAct.old.js b/packages/react-reconciler/src/ReactFiberAct.old.js index ddae518dcb631..bcb4ad707a1be 100644 --- a/packages/react-reconciler/src/ReactFiberAct.old.js +++ b/packages/react-reconciler/src/ReactFiberAct.old.js @@ -12,11 +12,32 @@ import type {Fiber} from './ReactFiber.old'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import {warnsIfNotActing} from './ReactFiberHostConfig'; -import {ConcurrentMode} from './ReactTypeOfMode'; const {ReactCurrentActQueue} = ReactSharedInternals; -export function isActEnvironment(fiber: Fiber) { +export function isLegacyActEnvironment(fiber: Fiber) { + if (__DEV__) { + // Legacy mode. We preserve the behavior of React 17's act. It assumes an + // act environment whenever `jest` is defined, but you can still turn off + // spurious warnings by setting IS_REACT_ACT_ENVIRONMENT explicitly + // to false. + + const isReactActEnvironmentGlobal = + // $FlowExpectedError – Flow doesn't know about IS_REACT_ACT_ENVIRONMENT global + typeof IS_REACT_ACT_ENVIRONMENT !== 'undefined' + ? IS_REACT_ACT_ENVIRONMENT + : undefined; + + // $FlowExpectedError - Flow doesn't know about jest + const jestIsDefined = typeof jest !== 'undefined'; + return ( + warnsIfNotActing && jestIsDefined && isReactActEnvironmentGlobal !== false + ); + } + return false; +} + +export function isConcurrentActEnvironment() { if (__DEV__) { const isReactActEnvironmentGlobal = // $FlowExpectedError – Flow doesn't know about IS_REACT_ACT_ENVIRONMENT global @@ -24,31 +45,14 @@ export function isActEnvironment(fiber: Fiber) { ? IS_REACT_ACT_ENVIRONMENT : undefined; - if (fiber.mode & ConcurrentMode) { - if ( - !isReactActEnvironmentGlobal && - ReactCurrentActQueue.current !== null - ) { - // TODO: Include link to relevant documentation page. - console.error( - 'The current testing environment is not configured to support ' + - 'act(...)', - ); - } - return isReactActEnvironmentGlobal; - } else { - // Legacy mode. We preserve the behavior of React 17's act. It assumes an - // act environment whenever `jest` is defined, but you can still turn off - // spurious warnings by setting IS_REACT_ACT_ENVIRONMENT explicitly - // to false. - // $FlowExpectedError - Flow doesn't know about jest - const jestIsDefined = typeof jest !== 'undefined'; - return ( - warnsIfNotActing && - jestIsDefined && - isReactActEnvironmentGlobal !== false + if (!isReactActEnvironmentGlobal && ReactCurrentActQueue.current !== null) { + // TODO: Include link to relevant documentation page. + console.error( + 'The current testing environment is not configured to support ' + + 'act(...)', ); } + return isReactActEnvironmentGlobal; } return false; } diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index d21704a1b6fea..266e98960cbb3 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -83,7 +83,6 @@ import { requestUpdateLane, requestEventTime, warnIfNotCurrentlyActingEffectsInDEV, - warnIfNotCurrentlyActingUpdatesInDev, markSkippedUpdateLanes, isInterleavedUpdate, } from './ReactFiberWorkLoop.new'; @@ -118,7 +117,6 @@ import { } from './ReactUpdateQueue.new'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; -import {isActEnvironment} from './ReactFiberAct.new'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -1679,9 +1677,7 @@ function mountEffect( deps: Array | void | null, ): void { if (__DEV__) { - if (isActEnvironment(currentlyRenderingFiber)) { - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); - } + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); } if ( __DEV__ && @@ -1709,9 +1705,7 @@ function updateEffect( deps: Array | void | null, ): void { if (__DEV__) { - if (isActEnvironment(currentlyRenderingFiber)) { - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); - } + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); } return updateEffectImpl(PassiveEffect, HookPassive, create, deps); } @@ -2196,12 +2190,6 @@ function dispatchReducerAction( enqueueRenderPhaseUpdate(queue, update); } else { enqueueUpdate(fiber, queue, update, lane); - - if (__DEV__) { - if (isActEnvironment(fiber)) { - warnIfNotCurrentlyActingUpdatesInDev(fiber); - } - } const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); if (root !== null) { @@ -2282,11 +2270,6 @@ function dispatchSetState( } } } - if (__DEV__) { - if (isActEnvironment(fiber)) { - warnIfNotCurrentlyActingUpdatesInDev(fiber); - } - } const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); if (root !== null) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index cf6786e617daf..b2378cbf30d65 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -83,7 +83,6 @@ import { requestUpdateLane, requestEventTime, warnIfNotCurrentlyActingEffectsInDEV, - warnIfNotCurrentlyActingUpdatesInDev, markSkippedUpdateLanes, isInterleavedUpdate, } from './ReactFiberWorkLoop.old'; @@ -118,7 +117,6 @@ import { } from './ReactUpdateQueue.old'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; -import {isActEnvironment} from './ReactFiberAct.old'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -1679,9 +1677,7 @@ function mountEffect( deps: Array | void | null, ): void { if (__DEV__) { - if (isActEnvironment(currentlyRenderingFiber)) { - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); - } + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); } if ( __DEV__ && @@ -1709,9 +1705,7 @@ function updateEffect( deps: Array | void | null, ): void { if (__DEV__) { - if (isActEnvironment(currentlyRenderingFiber)) { - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); - } + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); } return updateEffectImpl(PassiveEffect, HookPassive, create, deps); } @@ -2196,12 +2190,6 @@ function dispatchReducerAction( enqueueRenderPhaseUpdate(queue, update); } else { enqueueUpdate(fiber, queue, update, lane); - - if (__DEV__) { - if (isActEnvironment(fiber)) { - warnIfNotCurrentlyActingUpdatesInDev(fiber); - } - } const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); if (root !== null) { @@ -2282,11 +2270,6 @@ function dispatchSetState( } } } - if (__DEV__) { - if (isActEnvironment(fiber)) { - warnIfNotCurrentlyActingUpdatesInDev(fiber); - } - } const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); if (root !== null) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 930fc608f4724..2c065d6ee17ab 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -236,6 +236,10 @@ import { } from './ReactFiberDevToolsHook.new'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; import {releaseCache} from './ReactFiberCacheComponent.new'; +import { + isLegacyActEnvironment, + isConcurrentActEnvironment, +} from './ReactFiberAct.new'; const ceil = Math.ceil; @@ -493,6 +497,8 @@ export function scheduleUpdateOnFiber( } } + warnIfUpdatesNotWrappedWithActDEV(fiber); + if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) { if ( (executionContext & CommitContext) !== NoContext && @@ -2402,6 +2408,8 @@ export function pingSuspendedRoot( const eventTime = requestEventTime(); markRootPinged(root, pingedLanes, eventTime); + warnIfSuspenseResolutionNotWrappedWithActDEV(root); + if ( workInProgressRoot === root && isSubsetOfLanes(workInProgressRootRenderLanes, pingedLanes) @@ -2854,7 +2862,12 @@ function shouldForceFlushFallbacksInDEV() { export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { if (__DEV__) { + const isActEnvironment = + fiber.mode & ConcurrentMode + ? isConcurrentActEnvironment() + : isLegacyActEnvironment(fiber); if ( + isActEnvironment && (fiber.mode & StrictLegacyMode) !== NoMode && ReactCurrentActQueue.current === null ) { @@ -2875,12 +2888,36 @@ export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { } } -function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { +function warnIfUpdatesNotWrappedWithActDEV(fiber: Fiber): void { if (__DEV__) { - if ( - executionContext === NoContext && - ReactCurrentActQueue.current === null - ) { + if (fiber.mode & ConcurrentMode) { + if (!isConcurrentActEnvironment()) { + // Not in an act environment. No need to warn. + return; + } + } else { + // Legacy mode has additional cases where we suppress a warning. + if (!isLegacyActEnvironment(fiber)) { + // Not in an act environment. No need to warn. + return; + } + if (executionContext !== NoContext) { + // Legacy mode doesn't warn if the update is batched, i.e. + // batchedUpdates or flushSync. + return; + } + if ( + fiber.tag !== FunctionComponent && + fiber.tag !== ForwardRef && + fiber.tag !== SimpleMemoComponent + ) { + // For backwards compatibility with pre-hooks code, legacy mode only + // warns for updates that originate from a hook. + return; + } + } + + if (ReactCurrentActQueue.current === null) { const previousFiber = ReactCurrentFiberCurrent; try { setCurrentDebugFiberInDEV(fiber); @@ -2908,4 +2945,26 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { } } -export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV; +function warnIfSuspenseResolutionNotWrappedWithActDEV(root: FiberRoot): void { + if (__DEV__) { + if ( + root.tag !== LegacyRoot && + isConcurrentActEnvironment() && + ReactCurrentActQueue.current === null + ) { + console.error( + 'A suspended resource finished loading inside a test, but the event ' + + 'was not wrapped in act(...).\n\n' + + 'When testing, code that resolves suspended data should be wrapped ' + + 'into act(...):\n\n' + + 'act(() => {\n' + + ' /* finish loading suspended data */\n' + + '});\n' + + '/* assert on the output */\n\n' + + "This ensures that you're testing the behavior the user would see " + + 'in the browser.' + + ' Learn more at https://reactjs.org/link/wrap-tests-with-act', + ); + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 0b7f46c799016..97b835d3a18e4 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -236,6 +236,10 @@ import { } from './ReactFiberDevToolsHook.old'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; import {releaseCache} from './ReactFiberCacheComponent.old'; +import { + isLegacyActEnvironment, + isConcurrentActEnvironment, +} from './ReactFiberAct.old'; const ceil = Math.ceil; @@ -493,6 +497,8 @@ export function scheduleUpdateOnFiber( } } + warnIfUpdatesNotWrappedWithActDEV(fiber); + if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) { if ( (executionContext & CommitContext) !== NoContext && @@ -2402,6 +2408,8 @@ export function pingSuspendedRoot( const eventTime = requestEventTime(); markRootPinged(root, pingedLanes, eventTime); + warnIfSuspenseResolutionNotWrappedWithActDEV(root); + if ( workInProgressRoot === root && isSubsetOfLanes(workInProgressRootRenderLanes, pingedLanes) @@ -2854,7 +2862,12 @@ function shouldForceFlushFallbacksInDEV() { export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { if (__DEV__) { + const isActEnvironment = + fiber.mode & ConcurrentMode + ? isConcurrentActEnvironment() + : isLegacyActEnvironment(fiber); if ( + isActEnvironment && (fiber.mode & StrictLegacyMode) !== NoMode && ReactCurrentActQueue.current === null ) { @@ -2875,12 +2888,36 @@ export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { } } -function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { +function warnIfUpdatesNotWrappedWithActDEV(fiber: Fiber): void { if (__DEV__) { - if ( - executionContext === NoContext && - ReactCurrentActQueue.current === null - ) { + if (fiber.mode & ConcurrentMode) { + if (!isConcurrentActEnvironment()) { + // Not in an act environment. No need to warn. + return; + } + } else { + // Legacy mode has additional cases where we suppress a warning. + if (!isLegacyActEnvironment(fiber)) { + // Not in an act environment. No need to warn. + return; + } + if (executionContext !== NoContext) { + // Legacy mode doesn't warn if the update is batched, i.e. + // batchedUpdates or flushSync. + return; + } + if ( + fiber.tag !== FunctionComponent && + fiber.tag !== ForwardRef && + fiber.tag !== SimpleMemoComponent + ) { + // For backwards compatibility with pre-hooks code, legacy mode only + // warns for updates that originate from a hook. + return; + } + } + + if (ReactCurrentActQueue.current === null) { const previousFiber = ReactCurrentFiberCurrent; try { setCurrentDebugFiberInDEV(fiber); @@ -2908,4 +2945,26 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { } } -export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV; +function warnIfSuspenseResolutionNotWrappedWithActDEV(root: FiberRoot): void { + if (__DEV__) { + if ( + root.tag !== LegacyRoot && + isConcurrentActEnvironment() && + ReactCurrentActQueue.current === null + ) { + console.error( + 'A suspended resource finished loading inside a test, but the event ' + + 'was not wrapped in act(...).\n\n' + + 'When testing, code that resolves suspended data should be wrapped ' + + 'into act(...):\n\n' + + 'act(() => {\n' + + ' /* finish loading suspended data */\n' + + '});\n' + + '/* assert on the output */\n\n' + + "This ensures that you're testing the behavior the user would see " + + 'in the browser.' + + ' Learn more at https://reactjs.org/link/wrap-tests-with-act', + ); + } + } +} diff --git a/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js index ee93b3f992994..8be5b0bb9720e 100644 --- a/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js +++ b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js @@ -56,21 +56,17 @@ describe('DebugTracing', () => { expect(logs).toEqual([]); }); + // @gate build === 'development' // @gate experimental || www it('should not log anything for concurrent render without suspends or state updates', () => { - ReactTestRenderer.create( - -
- , - {unstable_isConcurrent: true}, + ReactTestRenderer.act(() => + ReactTestRenderer.create( + +
+ , + {unstable_isConcurrent: true}, + ), ); - - expect(logs).toEqual([]); - - logs.splice(0); - - expect(Scheduler).toFlushUntilNextPaint([]); - expect(logs).toEqual([]); }); @@ -81,12 +77,14 @@ describe('DebugTracing', () => { throw fakeSuspensePromise; } - ReactTestRenderer.create( - - - - - , + ReactTestRenderer.act(() => + ReactTestRenderer.create( + + + + + , + ), ); expect(logs).toEqual([ @@ -142,26 +140,33 @@ describe('DebugTracing', () => { // @gate experimental && build === 'development' && enableDebugTracing it('should log concurrent render with suspense', async () => { - const fakeSuspensePromise = Promise.resolve(true); + let isResolved = false; + let resolveFakeSuspensePromise; + const fakeSuspensePromise = new Promise(resolve => { + resolveFakeSuspensePromise = () => { + resolve(); + isResolved = true; + }; + }); + function Example() { - throw fakeSuspensePromise; + if (!isResolved) { + throw fakeSuspensePromise; + } + return null; } - ReactTestRenderer.create( - - - - - , - {unstable_isConcurrent: true}, + ReactTestRenderer.act(() => + ReactTestRenderer.create( + + + + + , + {unstable_isConcurrent: true}, + ), ); - expect(logs).toEqual([]); - - logs.splice(0); - - expect(Scheduler).toFlushUntilNextPaint([]); - expect(logs).toEqual([ `group: ⚛️ render (${DEFAULT_LANE_STRING})`, 'log: ⚛️ Example suspended', @@ -170,7 +175,7 @@ describe('DebugTracing', () => { logs.splice(0); - await fakeSuspensePromise; + await ReactTestRenderer.act(async () => await resolveFakeSuspensePromise()); expect(logs).toEqual(['log: ⚛️ Example resolved']); }); @@ -186,34 +191,23 @@ describe('DebugTracing', () => { return children; } - ReactTestRenderer.create( - - - - - - - , - {unstable_isConcurrent: true}, + ReactTestRenderer.act(() => + ReactTestRenderer.create( + + + + + + + , + {unstable_isConcurrent: true}, + ), ); - expect(logs).toEqual([]); - - logs.splice(0); - - expect(Scheduler).toFlushUntilNextPaint([]); - expect(logs).toEqual([ `group: ⚛️ render (${DEFAULT_LANE_STRING})`, 'log: ', `groupEnd: ⚛️ render (${DEFAULT_LANE_STRING})`, - ]); - - logs.splice(0); - - expect(Scheduler).toFlushUntilNextPaint([]); - - expect(logs).toEqual([ `group: ⚛️ render (${RETRY_LANE_STRING})`, 'log: ', `groupEnd: ⚛️ render (${RETRY_LANE_STRING})`, @@ -232,19 +226,15 @@ describe('DebugTracing', () => { } } - ReactTestRenderer.create( - - - , - {unstable_isConcurrent: true}, + ReactTestRenderer.act(() => + ReactTestRenderer.create( + + + , + {unstable_isConcurrent: true}, + ), ); - expect(logs).toEqual([]); - - logs.splice(0); - - expect(Scheduler).toFlushUntilNextPaint([]); - expect(logs).toEqual([ `group: ⚛️ commit (${DEFAULT_LANE_STRING})`, `group: ⚛️ layout effects (${DEFAULT_LANE_STRING})`, @@ -266,19 +256,15 @@ describe('DebugTracing', () => { } } - ReactTestRenderer.create( - - - , - {unstable_isConcurrent: true}, - ); - - expect(logs).toEqual([]); - - logs.splice(0); - expect(() => { - expect(Scheduler).toFlushUntilNextPaint([]); + ReactTestRenderer.act(() => + ReactTestRenderer.create( + + + , + {unstable_isConcurrent: true}, + ), + ); }).toErrorDev('Cannot update during an existing state transition'); expect(logs).toEqual([ @@ -298,19 +284,15 @@ describe('DebugTracing', () => { return didMount; } - ReactTestRenderer.create( - - - , - {unstable_isConcurrent: true}, + ReactTestRenderer.act(() => + ReactTestRenderer.create( + + + , + {unstable_isConcurrent: true}, + ), ); - expect(logs).toEqual([]); - - logs.splice(0); - - expect(Scheduler).toFlushUntilNextPaint([]); - expect(logs).toEqual([ `group: ⚛️ commit (${DEFAULT_LANE_STRING})`, `group: ⚛️ layout effects (${DEFAULT_LANE_STRING})`, @@ -378,19 +360,15 @@ describe('DebugTracing', () => { return null; } - ReactTestRenderer.create( - - - , - {unstable_isConcurrent: true}, + ReactTestRenderer.act(() => + ReactTestRenderer.create( + + + , + {unstable_isConcurrent: true}, + ), ); - expect(logs).toEqual([]); - - logs.splice(0); - - expect(Scheduler).toFlushUntilNextPaint([]); - expect(logs).toEqual([ `group: ⚛️ render (${DEFAULT_LANE_STRING})`, 'log: Hello from user code', @@ -398,6 +376,7 @@ describe('DebugTracing', () => { ]); }); + // @gate build === 'development' // @gate experimental || www it('should not log anything outside of a unstable_DebugTracingMode subtree', () => { function ExampleThatCascades() { @@ -417,16 +396,18 @@ describe('DebugTracing', () => { return null; } - ReactTestRenderer.create( - - - - - - - - - , + ReactTestRenderer.act(() => + ReactTestRenderer.create( + + + + + + + + + , + ), ); expect(logs).toEqual([]); diff --git a/packages/react-reconciler/src/__tests__/ReactActWarnings-test.js b/packages/react-reconciler/src/__tests__/ReactActWarnings-test.js index 058e01b1fe060..324d153273361 100644 --- a/packages/react-reconciler/src/__tests__/ReactActWarnings-test.js +++ b/packages/react-reconciler/src/__tests__/ReactActWarnings-test.js @@ -12,6 +12,10 @@ let Scheduler; let ReactNoop; let useState; let act; +let Suspense; +let startTransition; +let getCacheForType; +let caches; // These tests are mostly concerned with concurrent roots. The legacy root // behavior is covered by other older test suites and is unchanged from @@ -24,11 +28,110 @@ describe('act warnings', () => { ReactNoop = require('react-noop-renderer'); act = React.unstable_act; useState = React.useState; + Suspense = React.Suspense; + startTransition = React.startTransition; + getCacheForType = React.unstable_getCacheForType; + caches = []; }); - function Text(props) { - Scheduler.unstable_yieldValue(props.text); - return props.text; + function createTextCache() { + const data = new Map(); + const version = caches.length + 1; + const cache = { + version, + data, + resolve(text) { + const record = data.get(text); + if (record === undefined) { + const newRecord = { + status: 'resolved', + value: text, + }; + data.set(text, newRecord); + } else if (record.status === 'pending') { + const thenable = record.value; + record.status = 'resolved'; + record.value = text; + thenable.pings.forEach(t => t()); + } + }, + reject(text, error) { + const record = data.get(text); + if (record === undefined) { + const newRecord = { + status: 'rejected', + value: error, + }; + data.set(text, newRecord); + } else if (record.status === 'pending') { + const thenable = record.value; + record.status = 'rejected'; + record.value = error; + thenable.pings.forEach(t => t()); + } + }, + }; + caches.push(cache); + return cache; + } + + function readText(text) { + const textCache = getCacheForType(createTextCache); + const record = textCache.data.get(text); + if (record !== undefined) { + switch (record.status) { + case 'pending': + Scheduler.unstable_yieldValue(`Suspend! [${text}]`); + throw record.value; + case 'rejected': + Scheduler.unstable_yieldValue(`Error! [${text}]`); + throw record.value; + case 'resolved': + return textCache.version; + } + } else { + Scheduler.unstable_yieldValue(`Suspend! [${text}]`); + + const thenable = { + pings: [], + then(resolve) { + if (newRecord.status === 'pending') { + thenable.pings.push(resolve); + } else { + Promise.resolve().then(() => resolve(newRecord.value)); + } + }, + }; + + const newRecord = { + status: 'pending', + value: thenable, + }; + textCache.data.set(text, newRecord); + + throw thenable; + } + } + + function Text({text}) { + Scheduler.unstable_yieldValue(text); + return text; + } + + function AsyncText({text}) { + readText(text); + Scheduler.unstable_yieldValue(text); + return text; + } + + function resolveText(text) { + if (caches.length === 0) { + throw Error('Cache does not exist.'); + } else { + // Resolve the most recently created cache. An older cache can by + // resolved with `caches[index].resolve(text)`. + caches[caches.length - 1].resolve(text); + } } function withActEnvironment(value, scope) { @@ -127,4 +230,132 @@ describe('act warnings', () => { expect(root).toMatchRenderedOutput('1'); }); }); + + test('warns if root update is not wrapped', () => { + withActEnvironment(true, () => { + const root = ReactNoop.createRoot(); + expect(() => root.render('Hi')).toErrorDev( + // TODO: Better error message that doesn't make it look like "Root" is + // the name of a custom component + 'An update to Root inside a test was not wrapped in act(...)', + {withoutStack: true}, + ); + }); + }); + + // @gate __DEV__ + test('warns if class update is not wrapped', () => { + let app; + class App extends React.Component { + state = {count: 0}; + render() { + app = this; + return ; + } + } + + withActEnvironment(true, () => { + const root = ReactNoop.createRoot(); + act(() => { + root.render(); + }); + expect(() => app.setState({count: 1})).toErrorDev( + 'An update to App inside a test was not wrapped in act(...)', + ); + }); + }); + + // @gate __DEV__ + test('warns even if update is synchronous', () => { + let setState; + function App() { + const [state, _setState] = useState(0); + setState = _setState; + return ; + } + + withActEnvironment(true, () => { + const root = ReactNoop.createRoot(); + act(() => root.render()); + expect(Scheduler).toHaveYielded([0]); + expect(root).toMatchRenderedOutput('0'); + + // Even though this update is synchronous, we should still fire a warning, + // because it could have spawned additional asynchronous work + expect(() => ReactNoop.flushSync(() => setState(1))).toErrorDev( + 'An update to App inside a test was not wrapped in act(...)', + ); + + expect(Scheduler).toHaveYielded([1]); + expect(root).toMatchRenderedOutput('1'); + }); + }); + + // @gate __DEV__ + // @gate enableCache + test('warns if Suspense retry is not wrapped', () => { + function App() { + return ( + }> + + + ); + } + + withActEnvironment(true, () => { + const root = ReactNoop.createRoot(); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Suspend! [Async]', 'Loading...']); + expect(root).toMatchRenderedOutput('Loading...'); + + // This is a retry, not a ping, because we already showed a fallback. + expect(() => + resolveText('Async'), + ).toErrorDev( + 'A suspended resource finished loading inside a test, but the event ' + + 'was not wrapped in act(...)', + {withoutStack: true}, + ); + }); + }); + + // @gate __DEV__ + // @gate enableCache + test('warns if Suspense ping is not wrapped', () => { + function App({showMore}) { + return ( + }> + {showMore ? : } + + ); + } + + withActEnvironment(true, () => { + const root = ReactNoop.createRoot(); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['(empty)']); + expect(root).toMatchRenderedOutput('(empty)'); + + act(() => { + startTransition(() => { + root.render(); + }); + }); + expect(Scheduler).toHaveYielded(['Suspend! [Async]', 'Loading...']); + expect(root).toMatchRenderedOutput('(empty)'); + + // This is a ping, not a retry, because no fallback is showing. + expect(() => + resolveText('Async'), + ).toErrorDev( + 'A suspended resource finished loading inside a test, but the event ' + + 'was not wrapped in act(...)', + {withoutStack: true}, + ); + }); + }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js index 35c22e672ed3e..ee4bd306481b0 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js @@ -28,6 +28,8 @@ describe('ReactFiberHostContext', () => { .DefaultEventPriority; }); + global.IS_REACT_ACT_ENVIRONMENT = true; + // @gate __DEV__ it('works with null host context', async () => { let creates = 0; diff --git a/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js b/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js index 135757d24f9e1..78458bd2d5bfd 100644 --- a/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js @@ -23,12 +23,17 @@ describe('isomorphic act()', () => { act = React.unstable_act; }); + beforeEach(() => { + global.IS_REACT_ACT_ENVIRONMENT = true; + }); + // @gate __DEV__ test('bypasses queueMicrotask', async () => { const root = ReactNoop.createRoot(); // First test what happens without wrapping in act. This update would // normally be queued in a microtask. + global.IS_REACT_ACT_ENVIRONMENT = false; ReactNoop.unstable_runWithPriority(DiscreteEventPriority, () => { root.render('A'); }); @@ -40,6 +45,7 @@ describe('isomorphic act()', () => { // Now do the same thing but wrap the update with `act`. No // `await` necessary. + global.IS_REACT_ACT_ENVIRONMENT = true; act(() => { ReactNoop.unstable_runWithPriority(DiscreteEventPriority, () => { root.render('B'); From 3c4c1c4703e8f7362370c44a48ac9348229e3791 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 21 Oct 2021 18:39:59 -0400 Subject: [PATCH 061/109] Remove warning for dangling passive effects (#22609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In legacy mode, a test can get into a situation where passive effects are "dangling" — an update finished, and scheduled some passive effects, but the effects don't flush. This is why React warns if you don't wrap updates in act. The act API is responsible for flushing passive effects. But there are some cases where the act API (in legacy roots) intentionally doesn't warn, like updates that originate from roots and classes. It's possible those updates will render children that contain useEffect. Because of this, dangling effects are still possible, and React doesn't warn about it. So we implemented a second act warning for dangling effects. However, in concurrent roots, we now enforce that all APIs that schedule React work must be wrapped in act. There's no scenario where dangling passive effects can happen that doesn't already trigger the warning for updates. So the dangling effects warning is redundant. The warning was never part of a public release. It was only enabled in concurrent roots. So we can delete it. --- .../src/__tests__/ReactTestUtilsAct-test.js | 31 ---------------- .../src/ReactFiberHooks.new.js | 7 ---- .../src/ReactFiberHooks.old.js | 7 ---- .../src/ReactFiberWorkLoop.new.js | 35 +------------------ .../src/ReactFiberWorkLoop.old.js | 35 +------------------ 5 files changed, 2 insertions(+), 113 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js index 556772500ee2f..71adc286d08ba 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js @@ -98,43 +98,12 @@ describe('ReactTestUtils.act()', () => { }).toErrorDev([]); }); - it('warns in strict mode', () => { - expect(() => { - ReactDOM.render( - - - , - document.createElement('div'), - ); - }).toErrorDev([ - 'An update to App ran an effect, but was not wrapped in act(...)', - ]); - }); - // @gate __DEV__ it('does not warn in concurrent mode', () => { const root = ReactDOM.createRoot(document.createElement('div')); act(() => root.render()); Scheduler.unstable_flushAll(); }); - - it('warns in concurrent mode if root is strict', () => { - // TODO: We don't need this error anymore in concurrent mode because - // effects can only be scheduled as the result of an update, and we now - // enforce all updates must be wrapped with act, not just hook updates. - expect(() => { - const root = ReactDOM.createRoot(document.createElement('div'), { - unstable_strictMode: true, - }); - root.render(); - }).toErrorDev( - 'An update to Root inside a test was not wrapped in act(...)', - {withoutStack: true}, - ); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( - 'An update to App ran an effect, but was not wrapped in act(...)', - ); - }); }); }); diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 266e98960cbb3..c3b76549a22aa 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -82,7 +82,6 @@ import { scheduleUpdateOnFiber, requestUpdateLane, requestEventTime, - warnIfNotCurrentlyActingEffectsInDEV, markSkippedUpdateLanes, isInterleavedUpdate, } from './ReactFiberWorkLoop.new'; @@ -1676,9 +1675,6 @@ function mountEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { - if (__DEV__) { - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); - } if ( __DEV__ && enableStrictEffects && @@ -1704,9 +1700,6 @@ function updateEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { - if (__DEV__) { - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); - } return updateEffectImpl(PassiveEffect, HookPassive, create, deps); } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index b2378cbf30d65..8bc1510deb455 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -82,7 +82,6 @@ import { scheduleUpdateOnFiber, requestUpdateLane, requestEventTime, - warnIfNotCurrentlyActingEffectsInDEV, markSkippedUpdateLanes, isInterleavedUpdate, } from './ReactFiberWorkLoop.old'; @@ -1676,9 +1675,6 @@ function mountEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { - if (__DEV__) { - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); - } if ( __DEV__ && enableStrictEffects && @@ -1704,9 +1700,6 @@ function updateEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { - if (__DEV__) { - warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); - } return updateEffectImpl(PassiveEffect, HookPassive, create, deps); } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 2c065d6ee17ab..e35e5515bb0bf 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -97,12 +97,7 @@ import { createWorkInProgress, assignFiberPropertiesInDEV, } from './ReactFiber.new'; -import { - NoMode, - StrictLegacyMode, - ProfileMode, - ConcurrentMode, -} from './ReactTypeOfMode'; +import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode'; import { HostRoot, IndeterminateComponent, @@ -2860,34 +2855,6 @@ function shouldForceFlushFallbacksInDEV() { return __DEV__ && ReactCurrentActQueue.current !== null; } -export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { - if (__DEV__) { - const isActEnvironment = - fiber.mode & ConcurrentMode - ? isConcurrentActEnvironment() - : isLegacyActEnvironment(fiber); - if ( - isActEnvironment && - (fiber.mode & StrictLegacyMode) !== NoMode && - ReactCurrentActQueue.current === null - ) { - console.error( - 'An update to %s ran an effect, but was not wrapped in act(...).\n\n' + - 'When testing, code that causes React state updates should be ' + - 'wrapped into act(...):\n\n' + - 'act(() => {\n' + - ' /* fire events that update state */\n' + - '});\n' + - '/* assert on the output */\n\n' + - "This ensures that you're testing the behavior the user would see " + - 'in the browser.' + - ' Learn more at https://reactjs.org/link/wrap-tests-with-act', - getComponentNameFromFiber(fiber), - ); - } - } -} - function warnIfUpdatesNotWrappedWithActDEV(fiber: Fiber): void { if (__DEV__) { if (fiber.mode & ConcurrentMode) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 97b835d3a18e4..cde46adb01ab0 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -97,12 +97,7 @@ import { createWorkInProgress, assignFiberPropertiesInDEV, } from './ReactFiber.old'; -import { - NoMode, - StrictLegacyMode, - ProfileMode, - ConcurrentMode, -} from './ReactTypeOfMode'; +import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode'; import { HostRoot, IndeterminateComponent, @@ -2860,34 +2855,6 @@ function shouldForceFlushFallbacksInDEV() { return __DEV__ && ReactCurrentActQueue.current !== null; } -export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { - if (__DEV__) { - const isActEnvironment = - fiber.mode & ConcurrentMode - ? isConcurrentActEnvironment() - : isLegacyActEnvironment(fiber); - if ( - isActEnvironment && - (fiber.mode & StrictLegacyMode) !== NoMode && - ReactCurrentActQueue.current === null - ) { - console.error( - 'An update to %s ran an effect, but was not wrapped in act(...).\n\n' + - 'When testing, code that causes React state updates should be ' + - 'wrapped into act(...):\n\n' + - 'act(() => {\n' + - ' /* fire events that update state */\n' + - '});\n' + - '/* assert on the output */\n\n' + - "This ensures that you're testing the behavior the user would see " + - 'in the browser.' + - ' Learn more at https://reactjs.org/link/wrap-tests-with-act', - getComponentNameFromFiber(fiber), - ); - } - } -} - function warnIfUpdatesNotWrappedWithActDEV(fiber: Fiber): void { if (__DEV__) { if (fiber.mode & ConcurrentMode) { From 90e5d3638844414e1b9df0fe66c3fc46b211d210 Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Fri, 22 Oct 2021 11:44:31 -0500 Subject: [PATCH 062/109] chore: fix comment typo (#22615) --- packages/react-server/src/ReactFizzHooks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 6370f61b37735..2596632652b43 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -218,7 +218,7 @@ export function resetHooksState(): void { } function getCacheForType(resourceType: () => T): T { - // TODO: This should silently mark this as client rendered since it's not necesssarily + // TODO: This should silently mark this as client rendered since it's not necessarily // considered an error. It needs to work for things like Flight though. throw new Error('Not implemented.'); } From 4298ddbc56a711cecfa672ec8ac60c94a02c9380 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 24 Oct 2021 05:21:55 +0200 Subject: [PATCH 063/109] fix passing strings as chunks (#22617) --- .../src/server/ReactDOMServerFormatConfig.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index 1f5a7a65bed59..2012392004e52 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -471,7 +471,7 @@ function pushAttribute( attributeSeparator, attributeNameChunk, attributeAssign, - escapeTextForBrowser(value), + stringToChunk(escapeTextForBrowser(value)), attributeEnd, ); } @@ -482,7 +482,7 @@ function pushAttribute( attributeSeparator, attributeNameChunk, attributeAssign, - escapeTextForBrowser(value), + stringToChunk(escapeTextForBrowser(value)), attributeEnd, ); } @@ -493,7 +493,7 @@ function pushAttribute( attributeSeparator, attributeNameChunk, attributeAssign, - escapeTextForBrowser(value), + stringToChunk(escapeTextForBrowser(value)), attributeEnd, ); } @@ -510,7 +510,7 @@ function pushAttribute( attributeSeparator, attributeNameChunk, attributeAssign, - escapeTextForBrowser(value), + stringToChunk(escapeTextForBrowser(value)), attributeEnd, ); } @@ -532,7 +532,7 @@ function pushAttribute( attributeSeparator, stringToChunk(name), attributeAssign, - escapeTextForBrowser(value), + stringToChunk(escapeTextForBrowser(value)), attributeEnd, ); } @@ -1146,7 +1146,7 @@ function pushStartCustomElement( attributeSeparator, stringToChunk(propKey), attributeAssign, - escapeTextForBrowser(propValue), + stringToChunk(escapeTextForBrowser(propValue)), attributeEnd, ); } From 6c3dcc7a478f53aabd24b9712a7e506cf3702a64 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Wed, 27 Oct 2021 16:20:07 +0200 Subject: [PATCH 064/109] Enable 'Reload and Start Profiling' for Microsoft Edge (#22631) --- packages/react-devtools-extensions/src/main.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index 60ed0fcb1a9ec..f8750292a1a73 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -33,6 +33,7 @@ const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY = 'React::DevTools::supportsProfiling'; const isChrome = getBrowserName() === 'Chrome'; +const isEdge = getBrowserName() === 'Edge'; let panelCreated = false; @@ -203,7 +204,7 @@ function createPanelIfReactLoaded() { store = new Store(bridge, { isProfiling, - supportsReloadAndProfile: isChrome, + supportsReloadAndProfile: isChrome || isEdge, supportsProfiling, // At this time, the scheduling profiler can only parse Chrome performance profiles. supportsSchedulingProfiler: isChrome, From 26bc8ff9bfcdb67e255fea424b56909899ac208b Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 27 Oct 2021 16:34:44 -0400 Subject: [PATCH 065/109] Revert logic for checking for duplicate installations of DevTools (#22638) * Revert "Only show DevTools warning about unrecognized build in Chrome (#22571)" This reverts commit b72dc8e9300f5ae997f7f5cfcd79b604cca3df0c. * Revert "Show warning in UI when duplicate installations of DevTools extension are detected (#22563)" This reverts commit 930c9e7eeb4c9721e1b8dee074c2eef4d1eae5dc. * Revert "Prevent errors/crashing when multiple installs of DevTools are present (#22517)" This reverts commit 545d4c2de7934a43b0c5d3ce050d77b4c3113bd3. * Remove all references to passing extensionId in postMessage * Keep build changes * lint --- .../src/background.js | 22 +- .../src/checkForDuplicateInstallations.js | 137 ---- .../src/constants.js | 35 - .../src/contentScript.js | 5 - .../src/injectGlobalHook.js | 20 +- .../react-devtools-extensions/src/main.js | 762 ++++++++---------- .../src/devtools/views/DevTools.js | 4 - .../views/DuplicateInstallationDialog.js | 55 -- 8 files changed, 352 insertions(+), 688 deletions(-) delete mode 100644 packages/react-devtools-extensions/src/checkForDuplicateInstallations.js delete mode 100644 packages/react-devtools-extensions/src/constants.js delete mode 100644 packages/react-devtools-shared/src/devtools/views/DuplicateInstallationDialog.js diff --git a/packages/react-devtools-extensions/src/background.js b/packages/react-devtools-extensions/src/background.js index e8ff0f3f16202..9e09513b78fb4 100644 --- a/packages/react-devtools-extensions/src/background.js +++ b/packages/react-devtools-extensions/src/background.js @@ -1,20 +1,11 @@ -// @flow strict-local +/* global chrome */ 'use strict'; -declare var chrome: any; - -const ports: { - [tab: string]: {|devtools: any, 'content-script': any|}, -} = {}; +const ports = {}; const IS_FIREFOX = navigator.userAgent.indexOf('Firefox') >= 0; -import { - EXTENSION_INSTALL_CHECK, - SHOW_DUPLICATE_EXTENSION_WARNING, -} from './constants'; - chrome.runtime.onConnect.addListener(function(port) { let tab = null; let name = null; @@ -125,15 +116,6 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { } }); -chrome.runtime.onMessageExternal.addListener( - (request, sender, sendResponse) => { - if (request === EXTENSION_INSTALL_CHECK) { - sendResponse(true); - chrome.runtime.sendMessage(SHOW_DUPLICATE_EXTENSION_WARNING); - } - }, -); - chrome.runtime.onMessage.addListener((request, sender) => { const tab = sender.tab; if (tab) { diff --git a/packages/react-devtools-extensions/src/checkForDuplicateInstallations.js b/packages/react-devtools-extensions/src/checkForDuplicateInstallations.js deleted file mode 100644 index 01db8edb34f74..0000000000000 --- a/packages/react-devtools-extensions/src/checkForDuplicateInstallations.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * 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 strict-local - */ - -declare var chrome: any; - -import { - INTERNAL_EXTENSION_ID, - LOCAL_EXTENSION_ID, - __DEBUG__, -} from 'react-devtools-shared/src/constants'; -import {getBrowserName} from './utils'; -import { - EXTENSION_INSTALL_CHECK, - EXTENSION_INSTALLATION_TYPE, -} from './constants'; - -const IS_CHROME = getBrowserName() === 'Chrome'; - -const UNRECOGNIZED_EXTENSION_ERROR = - 'React Developer Tools: You are running an unrecognized installation of the React Developer Tools extension, which might conflict with other versions of the extension installed in your browser. ' + - 'Please make sure you only have a single version of the extension installed or enabled. ' + - 'If you are developing this extension locally, make sure to build the extension using the `yarn build::local` command.'; - -export function checkForDuplicateInstallations(callback: boolean => void) { - switch (EXTENSION_INSTALLATION_TYPE) { - case 'public': { - // If this is the public extension (e.g. from Chrome Web Store), check if an internal - // or local build of the extension is also installed, and if so, disable this extension. - // TODO show warning if other installations are present. - checkForInstalledExtensions([ - INTERNAL_EXTENSION_ID, - LOCAL_EXTENSION_ID, - ]).then(areExtensionsInstalled => { - if (areExtensionsInstalled.some(isInstalled => isInstalled)) { - callback(true); - } else { - callback(false); - } - }); - break; - } - case 'internal': { - // If this is the internal extension, check if a local build of the extension - // is also installed, and if so, disable this extension. - // If the public version of the extension is also installed, that extension - // will disable itself. - // TODO show warning if other installations are present. - checkForInstalledExtension(LOCAL_EXTENSION_ID).then(isInstalled => { - if (isInstalled) { - callback(true); - } else { - callback(false); - } - }); - break; - } - case 'local': { - if (__DEV__) { - // If this is the local extension (i.e. built locally during development), - // always keep this one enabled. Other installations disable themselves if - // they detect the local build is installed. - callback(false); - break; - } - - // If this extension wasn't built locally during development, we can't reliably - // detect if there are other installations of DevTools present. - // In this case, assume there are no duplicate exensions and show a warning about - // potential conflicts. - console.error(UNRECOGNIZED_EXTENSION_ERROR); - chrome.devtools.inspectedWindow.eval( - `console.error("${UNRECOGNIZED_EXTENSION_ERROR}")`, - ); - callback(false); - break; - } - case 'unknown': { - // TODO: Support duplicate extension detection in other browsers - if (IS_CHROME) { - // If we don't know how this extension was built, we can't reliably detect if there - // are other installations of DevTools present. - // In this case, assume there are no duplicate exensions and show a warning about - // potential conflicts. - console.error(UNRECOGNIZED_EXTENSION_ERROR); - chrome.devtools.inspectedWindow.eval( - `console.error("${UNRECOGNIZED_EXTENSION_ERROR}")`, - ); - } - callback(false); - break; - } - default: { - (EXTENSION_INSTALLATION_TYPE: empty); - } - } -} - -function checkForInstalledExtensions( - extensionIds: string[], -): Promise { - return Promise.all( - extensionIds.map(extensionId => checkForInstalledExtension(extensionId)), - ); -} - -function checkForInstalledExtension(extensionId: string): Promise { - return new Promise(resolve => { - chrome.runtime.sendMessage( - extensionId, - EXTENSION_INSTALL_CHECK, - response => { - if (__DEBUG__) { - console.log( - 'checkForDuplicateInstallations: Duplicate installation check responded with', - { - response, - error: chrome.runtime.lastError?.message, - currentExtension: EXTENSION_INSTALLATION_TYPE, - checkingExtension: extensionId, - }, - ); - } - if (chrome.runtime.lastError != null) { - resolve(false); - } else { - resolve(true); - } - }, - ); - }); -} diff --git a/packages/react-devtools-extensions/src/constants.js b/packages/react-devtools-extensions/src/constants.js deleted file mode 100644 index c17ad4d64acd8..0000000000000 --- a/packages/react-devtools-extensions/src/constants.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 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 strict-local - */ - -import { - CHROME_WEBSTORE_EXTENSION_ID, - INTERNAL_EXTENSION_ID, - LOCAL_EXTENSION_ID, -} from 'react-devtools-shared/src/constants'; - -declare var chrome: any; - -export const CURRENT_EXTENSION_ID = chrome.runtime.id; - -export const EXTENSION_INSTALL_CHECK = 'extension-install-check'; -export const SHOW_DUPLICATE_EXTENSION_WARNING = - 'show-duplicate-extension-warning'; - -export const EXTENSION_INSTALLATION_TYPE: - | 'public' - | 'internal' - | 'local' - | 'unknown' = - CURRENT_EXTENSION_ID === CHROME_WEBSTORE_EXTENSION_ID - ? 'public' - : CURRENT_EXTENSION_ID === INTERNAL_EXTENSION_ID - ? 'internal' - : CURRENT_EXTENSION_ID === LOCAL_EXTENSION_ID - ? 'local' - : 'unknown'; diff --git a/packages/react-devtools-extensions/src/contentScript.js b/packages/react-devtools-extensions/src/contentScript.js index 179959f7e01ec..c914c6e7b3dfc 100644 --- a/packages/react-devtools-extensions/src/contentScript.js +++ b/packages/react-devtools-extensions/src/contentScript.js @@ -2,8 +2,6 @@ 'use strict'; -import {CURRENT_EXTENSION_ID} from './constants'; - let backendDisconnected: boolean = false; let backendInitialized: boolean = false; @@ -12,7 +10,6 @@ function sayHelloToBackend() { { source: 'react-devtools-content-script', hello: true, - extensionId: CURRENT_EXTENSION_ID, }, '*', ); @@ -23,7 +20,6 @@ function handleMessageFromDevtools(message) { { source: 'react-devtools-content-script', payload: message, - extensionId: CURRENT_EXTENSION_ID, }, '*', ); @@ -53,7 +49,6 @@ function handleDisconnect() { type: 'event', event: 'shutdown', }, - extensionId: CURRENT_EXTENSION_ID, }, '*', ); diff --git a/packages/react-devtools-extensions/src/injectGlobalHook.js b/packages/react-devtools-extensions/src/injectGlobalHook.js index a5d96966c7e10..79e5a84adba88 100644 --- a/packages/react-devtools-extensions/src/injectGlobalHook.js +++ b/packages/react-devtools-extensions/src/injectGlobalHook.js @@ -2,11 +2,7 @@ import nullthrows from 'nullthrows'; import {installHook} from 'react-devtools-shared/src/hook'; -import { - __DEBUG__, - SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, -} from 'react-devtools-shared/src/constants'; -import {CURRENT_EXTENSION_ID, EXTENSION_INSTALLATION_TYPE} from './constants'; +import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants'; import {sessionStorageGetItem} from 'react-devtools-shared/src/storage'; function injectCode(code) { @@ -31,19 +27,6 @@ window.addEventListener('message', function onMessage({data, source}) { if (source !== window || !data) { return; } - if (data.extensionId != null && data.extensionId !== CURRENT_EXTENSION_ID) { - if (__DEBUG__) { - console.log( - `[injectGlobalHook] Received message '${data.source}' from different extension instance. Skipping message.`, - { - currentExtension: EXTENSION_INSTALLATION_TYPE, - currentExtensionId: CURRENT_EXTENSION_ID, - providedExtensionId: data.extensionId, - }, - ); - } - return; - } switch (data.source) { case 'react-devtools-detector': lastDetectionResult = { @@ -118,7 +101,6 @@ window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on('renderer', function({reactBuildType}) window.postMessage({ source: 'react-devtools-detector', reactBuildType, - extensionId: "${CURRENT_EXTENSION_ID}", }, '*'); }); `; diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index f8750292a1a73..6a3836839a4ea 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -22,18 +22,11 @@ import { import DevTools from 'react-devtools-shared/src/devtools/views/DevTools'; import {__DEBUG__} from 'react-devtools-shared/src/constants'; import {logEvent} from 'react-devtools-shared/src/Logger'; -import { - CURRENT_EXTENSION_ID, - EXTENSION_INSTALLATION_TYPE, - SHOW_DUPLICATE_EXTENSION_WARNING, -} from './constants'; -import {checkForDuplicateInstallations} from './checkForDuplicateInstallations'; const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY = 'React::DevTools::supportsProfiling'; const isChrome = getBrowserName() === 'Chrome'; -const isEdge = getBrowserName() === 'Edge'; let panelCreated = false; @@ -77,186 +70,135 @@ function createPanelIfReactLoaded() { return; } - checkForDuplicateInstallations(hasDuplicateInstallation => { - if (hasDuplicateInstallation) { - if (__DEBUG__) { - console.log( - '[main] createPanelIfReactLoaded: Duplicate installation detected, skipping initialization of extension.', - {currentExtension: EXTENSION_INSTALLATION_TYPE}, - ); - } - panelCreated = true; - clearInterval(loadCheckInterval); - return; - } + panelCreated = true; - if (__DEBUG__) { - console.log( - '[main] createPanelIfReactLoaded: No duplicate installations detected, continuing with initialization.', - {currentExtension: EXTENSION_INSTALLATION_TYPE}, - ); - } - - panelCreated = true; + clearInterval(loadCheckInterval); - clearInterval(loadCheckInterval); + let bridge = null; + let store = null; - let bridge = null; - let store = null; + let profilingData = null; - let profilingData = null; + let componentsPortalContainer = null; + let profilerPortalContainer = null; - let componentsPortalContainer = null; - let profilerPortalContainer = null; + let cloneStyleTags = null; + let mostRecentOverrideTab = null; + let render = null; + let root = null; - let cloneStyleTags = null; - let mostRecentOverrideTab = null; - let render = null; - let root = null; - let warnIfDuplicateInstallation = false; + const tabId = chrome.devtools.inspectedWindow.tabId; - const tabId = chrome.devtools.inspectedWindow.tabId; + registerDevToolsEventLogger('extension'); - registerDevToolsEventLogger('extension'); - - function onDuplicateExtensionMessage(message) { - if (message === SHOW_DUPLICATE_EXTENSION_WARNING) { - chrome.runtime.onMessage.removeListener( - onDuplicateExtensionMessage, - ); + function initBridgeAndStore() { + const port = chrome.runtime.connect({ + name: String(tabId), + }); + // Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation, + // so it makes no sense to handle it here. + + bridge = new Bridge({ + listen(fn) { + const listener = message => fn(message); + // Store the reference so that we unsubscribe from the same object. + const portOnMessage = port.onMessage; + portOnMessage.addListener(listener); + return () => { + portOnMessage.removeListener(listener); + }; + }, + send(event: string, payload: any, transferable?: Array) { + port.postMessage({event, payload}, transferable); + }, + }); + bridge.addListener('reloadAppForProfiling', () => { + localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true'); + chrome.devtools.inspectedWindow.eval('window.location.reload();'); + }); + bridge.addListener('syncSelectionToNativeElementsPanel', () => { + setBrowserSelectionFromReact(); + }); - if (warnIfDuplicateInstallation === true) { - return; - } - warnIfDuplicateInstallation = true; - const errorMessage = - 'React Developer Tools: We detected that there are multiple versions of React Developer Tools ' + - 'installed and enabled in your browser at the same time, which will cause ' + - 'issues while using the extension. ' + - 'Please ensure that you have installed and enabled only a single ' + - 'version of React Developer Tools before proceeding.'; - console.error(errorMessage); - chrome.devtools.inspectedWindow.eval( - `console.error("${errorMessage}")`, - ); - if (render != null) { - render(); - } - } + // This flag lets us tip the Store off early that we expect to be profiling. + // This avoids flashing a temporary "Profiling not supported" message in the Profiler tab, + // after a user has clicked the "reload and profile" button. + let isProfiling = false; + let supportsProfiling = false; + if ( + localStorageGetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY) === 'true' + ) { + supportsProfiling = true; + isProfiling = true; + localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY); } - chrome.runtime.onMessage.addListener(onDuplicateExtensionMessage); - - function initBridgeAndStore() { - const port = chrome.runtime.connect({ - name: String(tabId), - }); - // Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation, - // so it makes no sense to handle it here. - - bridge = new Bridge({ - listen(fn) { - const listener = message => fn(message); - // Store the reference so that we unsubscribe from the same object. - const portOnMessage = port.onMessage; - portOnMessage.addListener(listener); - return () => { - portOnMessage.removeListener(listener); - }; - }, - send(event: string, payload: any, transferable?: Array) { - port.postMessage({event, payload}, transferable); - }, - }); - bridge.addListener('reloadAppForProfiling', () => { - localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true'); - chrome.devtools.inspectedWindow.eval('window.location.reload();'); - }); - bridge.addListener('syncSelectionToNativeElementsPanel', () => { - setBrowserSelectionFromReact(); - }); - // This flag lets us tip the Store off early that we expect to be profiling. - // This avoids flashing a temporary "Profiling not supported" message in the Profiler tab, - // after a user has clicked the "reload and profile" button. - let isProfiling = false; - let supportsProfiling = false; - if ( - localStorageGetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY) === 'true' - ) { - supportsProfiling = true; - isProfiling = true; - localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY); - } + if (store !== null) { + profilingData = store.profilerStore.profilingData; + } - if (store !== null) { - profilingData = store.profilerStore.profilingData; - } + bridge.addListener('extensionBackendInitialized', () => { + // Initialize the renderer's trace-updates setting. + // This handles the case of navigating to a new page after the DevTools have already been shown. + bridge.send( + 'setTraceUpdatesEnabled', + localStorageGetItem(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY) === + 'true', + ); + }); - bridge.addListener('extensionBackendInitialized', () => { - // Initialize the renderer's trace-updates setting. - // This handles the case of navigating to a new page after the DevTools have already been shown. - bridge.send( - 'setTraceUpdatesEnabled', - localStorageGetItem(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY) === - 'true', - ); - }); + store = new Store(bridge, { + isProfiling, + supportsReloadAndProfile: isChrome, + supportsProfiling, + // At this time, the scheduling profiler can only parse Chrome performance profiles. + supportsSchedulingProfiler: isChrome, + supportsTraceUpdates: true, + }); + store.profilerStore.profilingData = profilingData; + + // Initialize the backend only once the Store has been initialized. + // Otherwise the Store may miss important initial tree op codes. + chrome.devtools.inspectedWindow.eval( + `window.postMessage({ source: 'react-devtools-inject-backend' }, '*');`, + function(response, evalError) { + if (evalError) { + console.error(evalError); + } + }, + ); - store = new Store(bridge, { - isProfiling, - supportsReloadAndProfile: isChrome || isEdge, - supportsProfiling, - // At this time, the scheduling profiler can only parse Chrome performance profiles. - supportsSchedulingProfiler: isChrome, - supportsTraceUpdates: true, - }); - store.profilerStore.profilingData = profilingData; - - // Initialize the backend only once the Store has been initialized. - // Otherwise the Store may miss important initial tree op codes. - chrome.devtools.inspectedWindow.eval( - `window.postMessage({ - source: 'react-devtools-inject-backend', - extensionId: "${CURRENT_EXTENSION_ID}", - }, '*');`, - function(response, evalError) { - if (evalError) { - console.error(evalError); - } - }, - ); + const viewAttributeSourceFunction = (id, path) => { + const rendererID = store.getRendererIDForElement(id); + if (rendererID != null) { + // Ask the renderer interface to find the specified attribute, + // and store it as a global variable on the window. + bridge.send('viewAttributeSource', {id, path, rendererID}); - const viewAttributeSourceFunction = (id, path) => { - const rendererID = store.getRendererIDForElement(id); - if (rendererID != null) { - // Ask the renderer interface to find the specified attribute, - // and store it as a global variable on the window. - bridge.send('viewAttributeSource', {id, path, rendererID}); - - setTimeout(() => { - // Ask Chrome to display the location of the attribute, - // assuming the renderer found a match. - chrome.devtools.inspectedWindow.eval(` + setTimeout(() => { + // Ask Chrome to display the location of the attribute, + // assuming the renderer found a match. + chrome.devtools.inspectedWindow.eval(` if (window.$attribute != null) { inspect(window.$attribute); } `); - }, 100); - } - }; + }, 100); + } + }; - const viewElementSourceFunction = id => { - const rendererID = store.getRendererIDForElement(id); - if (rendererID != null) { - // Ask the renderer interface to determine the component function, - // and store it as a global variable on the window - bridge.send('viewElementSource', {id, rendererID}); - - setTimeout(() => { - // Ask Chrome to display the location of the component function, - // or a render method if it is a Class (ideally Class instance, not type) - // assuming the renderer found one. - chrome.devtools.inspectedWindow.eval(` + const viewElementSourceFunction = id => { + const rendererID = store.getRendererIDForElement(id); + if (rendererID != null) { + // Ask the renderer interface to determine the component function, + // and store it as a global variable on the window + bridge.send('viewElementSource', {id, rendererID}); + + setTimeout(() => { + // Ask Chrome to display the location of the component function, + // or a render method if it is a Class (ideally Class instance, not type) + // assuming the renderer found one. + chrome.devtools.inspectedWindow.eval(` if (window.$type != null) { if ( window.$type && @@ -271,294 +213,288 @@ function createPanelIfReactLoaded() { } } `); - }, 100); - } - }; + }, 100); + } + }; - let debugIDCounter = 0; + let debugIDCounter = 0; - // For some reason in Firefox, chrome.runtime.sendMessage() from a content script - // never reaches the chrome.runtime.onMessage event listener. - let fetchFileWithCaching = null; - if (isChrome) { - const fetchFromNetworkCache = (url, resolve, reject) => { - // Debug ID allows us to avoid re-logging (potentially long) URL strings below, - // while also still associating (potentially) interleaved logs with the original request. - let debugID = null; + // For some reason in Firefox, chrome.runtime.sendMessage() from a content script + // never reaches the chrome.runtime.onMessage event listener. + let fetchFileWithCaching = null; + if (isChrome) { + const fetchFromNetworkCache = (url, resolve, reject) => { + // Debug ID allows us to avoid re-logging (potentially long) URL strings below, + // while also still associating (potentially) interleaved logs with the original request. + let debugID = null; - if (__DEBUG__) { - debugID = debugIDCounter++; - console.log(`[main] fetchFromNetworkCache(${debugID})`, url); - } + if (__DEBUG__) { + debugID = debugIDCounter++; + console.log(`[main] fetchFromNetworkCache(${debugID})`, url); + } - chrome.devtools.network.getHAR(harLog => { - for (let i = 0; i < harLog.entries.length; i++) { - const entry = harLog.entries[i]; - if (url === entry.request.url) { - if (__DEBUG__) { - console.log( - `[main] fetchFromNetworkCache(${debugID}) Found matching URL in HAR`, - url, - ); - } + chrome.devtools.network.getHAR(harLog => { + for (let i = 0; i < harLog.entries.length; i++) { + const entry = harLog.entries[i]; + if (url === entry.request.url) { + if (__DEBUG__) { + console.log( + `[main] fetchFromNetworkCache(${debugID}) Found matching URL in HAR`, + url, + ); + } - entry.getContent(content => { - if (content) { - if (__DEBUG__) { - console.log( - `[main] fetchFromNetworkCache(${debugID}) Content retrieved`, - ); - } - - resolve(content); - } else { - if (__DEBUG__) { - console.log( - `[main] fetchFromNetworkCache(${debugID}) Invalid content returned by getContent()`, - content, - ); - } - - // Edge case where getContent() returned null; fall back to fetch. - fetchFromPage(url, resolve, reject); + entry.getContent(content => { + if (content) { + if (__DEBUG__) { + console.log( + `[main] fetchFromNetworkCache(${debugID}) Content retrieved`, + ); } - }); - return; - } - } + resolve(content); + } else { + if (__DEBUG__) { + console.log( + `[main] fetchFromNetworkCache(${debugID}) Invalid content returned by getContent()`, + content, + ); + } - if (__DEBUG__) { - console.log( - `[main] fetchFromNetworkCache(${debugID}) No cached request found in getHAR()`, - ); - } + // Edge case where getContent() returned null; fall back to fetch. + fetchFromPage(url, resolve, reject); + } + }); - // No matching URL found; fall back to fetch. - fetchFromPage(url, resolve, reject); - }); - }; + return; + } + } - const fetchFromPage = (url, resolve, reject) => { if (__DEBUG__) { - console.log('[main] fetchFromPage()', url); + console.log( + `[main] fetchFromNetworkCache(${debugID}) No cached request found in getHAR()`, + ); } - function onPortMessage({payload, source}) { - if (source === 'react-devtools-content-script') { - switch (payload?.type) { - case 'fetch-file-with-cache-complete': - chrome.runtime.onMessage.removeListener(onPortMessage); - resolve(payload.value); - break; - case 'fetch-file-with-cache-error': - chrome.runtime.onMessage.removeListener(onPortMessage); - reject(payload.value); - break; - } + // No matching URL found; fall back to fetch. + fetchFromPage(url, resolve, reject); + }); + }; + + const fetchFromPage = (url, resolve, reject) => { + if (__DEBUG__) { + console.log('[main] fetchFromPage()', url); + } + + function onPortMessage({payload, source}) { + if (source === 'react-devtools-content-script') { + switch (payload?.type) { + case 'fetch-file-with-cache-complete': + chrome.runtime.onMessage.removeListener(onPortMessage); + resolve(payload.value); + break; + case 'fetch-file-with-cache-error': + chrome.runtime.onMessage.removeListener(onPortMessage); + reject(payload.value); + break; } } + } - chrome.runtime.onMessage.addListener(onPortMessage); + chrome.runtime.onMessage.addListener(onPortMessage); - chrome.devtools.inspectedWindow.eval(` - window.postMessage({ - source: 'react-devtools-extension', - extensionId: "${CURRENT_EXTENSION_ID}", - payload: { - type: 'fetch-file-with-cache', - url: "${url}", - }, - }, '*'); - `); - }; - - // Fetching files from the extension won't make use of the network cache - // for resources that have already been loaded by the page. - // This helper function allows the extension to request files to be fetched - // by the content script (running in the page) to increase the likelihood of a cache hit. - fetchFileWithCaching = url => { - return new Promise((resolve, reject) => { - // Try fetching from the Network cache first. - // If DevTools was opened after the page started loading, we may have missed some requests. - // So fall back to a fetch() from the page and hope we get a cached response that way. - fetchFromNetworkCache(url, resolve, reject); + chrome.devtools.inspectedWindow.eval(` + window.postMessage({ + source: 'react-devtools-extension', + payload: { + type: 'fetch-file-with-cache', + url: "${url}", + }, }); - }; - } - - // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. - const hookNamesModuleLoaderFunction = () => - import( - /* webpackChunkName: 'parseHookNames' */ 'react-devtools-shared/src/hooks/parseHookNames' - ); - - root = createRoot(document.createElement('div')); - - render = (overrideTab = mostRecentOverrideTab) => { - mostRecentOverrideTab = overrideTab; - root.render( - createElement(DevTools, { - bridge, - browserTheme: getBrowserTheme(), - componentsPortalContainer, - enabledInspectedElementContextMenu: true, - fetchFileWithCaching, - hookNamesModuleLoaderFunction, - overrideTab, - profilerPortalContainer, - warnIfDuplicateInstallation, - showTabBar: false, - store, - warnIfUnsupportedVersionDetected: true, - viewAttributeSourceFunction, - viewElementSourceFunction, - }), - ); + `); }; - render(); + // Fetching files from the extension won't make use of the network cache + // for resources that have already been loaded by the page. + // This helper function allows the extension to request files to be fetched + // by the content script (running in the page) to increase the likelihood of a cache hit. + fetchFileWithCaching = url => { + return new Promise((resolve, reject) => { + // Try fetching from the Network cache first. + // If DevTools was opened after the page started loading, we may have missed some requests. + // So fall back to a fetch() from the page and hope we get a cached response that way. + fetchFromNetworkCache(url, resolve, reject); + }); + }; } - cloneStyleTags = () => { - const linkTags = []; - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const linkTag of document.getElementsByTagName('link')) { - if (linkTag.rel === 'stylesheet') { - const newLinkTag = document.createElement('link'); - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const attribute of linkTag.attributes) { - newLinkTag.setAttribute( - attribute.nodeName, - attribute.nodeValue, - ); - } - linkTags.push(newLinkTag); - } - } - return linkTags; + // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. + const hookNamesModuleLoaderFunction = () => + import( + /* webpackChunkName: 'parseHookNames' */ 'react-devtools-shared/src/hooks/parseHookNames' + ); + + root = createRoot(document.createElement('div')); + + render = (overrideTab = mostRecentOverrideTab) => { + mostRecentOverrideTab = overrideTab; + root.render( + createElement(DevTools, { + bridge, + browserTheme: getBrowserTheme(), + componentsPortalContainer, + enabledInspectedElementContextMenu: true, + fetchFileWithCaching, + hookNamesModuleLoaderFunction, + overrideTab, + profilerPortalContainer, + showTabBar: false, + store, + warnIfUnsupportedVersionDetected: true, + viewAttributeSourceFunction, + viewElementSourceFunction, + }), + ); }; - initBridgeAndStore(); + render(); + } - function ensureInitialHTMLIsCleared(container) { - if (container._hasInitialHTMLBeenCleared) { - return; + cloneStyleTags = () => { + const linkTags = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const linkTag of document.getElementsByTagName('link')) { + if (linkTag.rel === 'stylesheet') { + const newLinkTag = document.createElement('link'); + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const attribute of linkTag.attributes) { + newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue); + } + linkTags.push(newLinkTag); } - container.innerHTML = ''; - container._hasInitialHTMLBeenCleared = true; } + return linkTags; + }; - function setBrowserSelectionFromReact() { - // This is currently only called on demand when you press "view DOM". - // In the future, if Chrome adds an inspect() that doesn't switch tabs, - // we could make this happen automatically when you select another component. - chrome.devtools.inspectedWindow.eval( - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + - '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' + - 'false', - (didSelectionChange, evalError) => { - if (evalError) { - console.error(evalError); - } - }, - ); - } + initBridgeAndStore(); - function setReactSelectionFromBrowser() { - // When the user chooses a different node in the browser Elements tab, - // copy it over to the hook object so that we can sync the selection. - chrome.devtools.inspectedWindow.eval( - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' + - 'false', - (didSelectionChange, evalError) => { - if (evalError) { - console.error(evalError); - } else if (didSelectionChange) { - // Remember to sync the selection next time we show Components tab. - needsToSyncElementSelection = true; - } - }, - ); + function ensureInitialHTMLIsCleared(container) { + if (container._hasInitialHTMLBeenCleared) { + return; } + container.innerHTML = ''; + container._hasInitialHTMLBeenCleared = true; + } - setReactSelectionFromBrowser(); - chrome.devtools.panels.elements.onSelectionChanged.addListener(() => { - setReactSelectionFromBrowser(); - }); + function setBrowserSelectionFromReact() { + // This is currently only called on demand when you press "view DOM". + // In the future, if Chrome adds an inspect() that doesn't switch tabs, + // we could make this happen automatically when you select another component. + chrome.devtools.inspectedWindow.eval( + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + + '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' + + 'false', + (didSelectionChange, evalError) => { + if (evalError) { + console.error(evalError); + } + }, + ); + } - let currentPanel = null; - let needsToSyncElementSelection = false; - - chrome.devtools.panels.create( - isChrome ? '⚛️ Components' : 'Components', - '', - 'panel.html', - extensionPanel => { - extensionPanel.onShown.addListener(panel => { - if (needsToSyncElementSelection) { - needsToSyncElementSelection = false; - bridge.send('syncSelectionFromNativeElementsPanel'); - } + function setReactSelectionFromBrowser() { + // When the user chooses a different node in the browser Elements tab, + // copy it over to the hook object so that we can sync the selection. + chrome.devtools.inspectedWindow.eval( + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' + + 'false', + (didSelectionChange, evalError) => { + if (evalError) { + console.error(evalError); + } else if (didSelectionChange) { + // Remember to sync the selection next time we show Components tab. + needsToSyncElementSelection = true; + } + }, + ); + } - if (currentPanel === panel) { - return; - } + setReactSelectionFromBrowser(); + chrome.devtools.panels.elements.onSelectionChanged.addListener(() => { + setReactSelectionFromBrowser(); + }); - currentPanel = panel; - componentsPortalContainer = panel.container; + let currentPanel = null; + let needsToSyncElementSelection = false; + + chrome.devtools.panels.create( + isChrome ? '⚛️ Components' : 'Components', + '', + 'panel.html', + extensionPanel => { + extensionPanel.onShown.addListener(panel => { + if (needsToSyncElementSelection) { + needsToSyncElementSelection = false; + bridge.send('syncSelectionFromNativeElementsPanel'); + } - if (componentsPortalContainer != null) { - ensureInitialHTMLIsCleared(componentsPortalContainer); - render('components'); - panel.injectStyles(cloneStyleTags); - logEvent({event_name: 'selected-components-tab'}); - } - }); - extensionPanel.onHidden.addListener(panel => { - // TODO: Stop highlighting and stuff. - }); - }, - ); + if (currentPanel === panel) { + return; + } - chrome.devtools.panels.create( - isChrome ? '⚛️ Profiler' : 'Profiler', - '', - 'panel.html', - extensionPanel => { - extensionPanel.onShown.addListener(panel => { - if (currentPanel === panel) { - return; - } + currentPanel = panel; + componentsPortalContainer = panel.container; - currentPanel = panel; - profilerPortalContainer = panel.container; + if (componentsPortalContainer != null) { + ensureInitialHTMLIsCleared(componentsPortalContainer); + render('components'); + panel.injectStyles(cloneStyleTags); + logEvent({event_name: 'selected-components-tab'}); + } + }); + extensionPanel.onHidden.addListener(panel => { + // TODO: Stop highlighting and stuff. + }); + }, + ); + + chrome.devtools.panels.create( + isChrome ? '⚛️ Profiler' : 'Profiler', + '', + 'panel.html', + extensionPanel => { + extensionPanel.onShown.addListener(panel => { + if (currentPanel === panel) { + return; + } - if (profilerPortalContainer != null) { - ensureInitialHTMLIsCleared(profilerPortalContainer); - render('profiler'); - panel.injectStyles(cloneStyleTags); - logEvent({event_name: 'selected-profiler-tab'}); - } - }); - }, - ); + currentPanel = panel; + profilerPortalContainer = panel.container; + + if (profilerPortalContainer != null) { + ensureInitialHTMLIsCleared(profilerPortalContainer); + render('profiler'); + panel.injectStyles(cloneStyleTags); + logEvent({event_name: 'selected-profiler-tab'}); + } + }); + }, + ); - chrome.devtools.network.onNavigated.removeListener(checkPageForReact); + chrome.devtools.network.onNavigated.removeListener(checkPageForReact); - // Re-initialize DevTools panel when a new page is loaded. - chrome.devtools.network.onNavigated.addListener(function onNavigated() { - // Re-initialize saved filters on navigation, - // since global values stored on window get reset in this case. - syncSavedPreferences(); + // Re-initialize DevTools panel when a new page is loaded. + chrome.devtools.network.onNavigated.addListener(function onNavigated() { + // Re-initialize saved filters on navigation, + // since global values stored on window get reset in this case. + syncSavedPreferences(); - // It's easiest to recreate the DevTools panel (to clean up potential stale state). - // We can revisit this in the future as a small optimization. - flushSync(() => root.unmount()); + // It's easiest to recreate the DevTools panel (to clean up potential stale state). + // We can revisit this in the future as a small optimization. + flushSync(() => root.unmount()); - initBridgeAndStore(); - }); + initBridgeAndStore(); }); }, ); diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index 8f1b6a22c11bc..781ad9d902f68 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -34,7 +34,6 @@ import {SchedulingProfilerContextController} from 'react-devtools-scheduling-pro import {ModalDialogContextController} from './ModalDialog'; import ReactLogo from './ReactLogo'; import UnsupportedBridgeProtocolDialog from './UnsupportedBridgeProtocolDialog'; -import DuplicateInstallationDialog from './DuplicateInstallationDialog'; import UnsupportedVersionDialog from './UnsupportedVersionDialog'; import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected'; import {useLocalStorage} from './hooks'; @@ -74,7 +73,6 @@ export type Props = {| enabledInspectedElementContextMenu?: boolean, showTabBar?: boolean, store: Store, - warnIfDuplicateInstallation?: boolean, warnIfLegacyBackendDetected?: boolean, warnIfUnsupportedVersionDetected?: boolean, viewAttributeSourceFunction?: ?ViewAttributeSource, @@ -134,7 +132,6 @@ export default function DevTools({ profilerPortalContainer, showTabBar = false, store, - warnIfDuplicateInstallation = false, warnIfLegacyBackendDetected = false, warnIfUnsupportedVersionDetected = false, viewAttributeSourceFunction, @@ -322,7 +319,6 @@ export default function DevTools({ - {warnIfDuplicateInstallation && } {warnIfLegacyBackendDetected && } {warnIfUnsupportedVersionDetected && } diff --git a/packages/react-devtools-shared/src/devtools/views/DuplicateInstallationDialog.js b/packages/react-devtools-shared/src/devtools/views/DuplicateInstallationDialog.js deleted file mode 100644 index 12a32ce4b7f43..0000000000000 --- a/packages/react-devtools-shared/src/devtools/views/DuplicateInstallationDialog.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 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 strict-local - */ - -import * as React from 'react'; -import {Fragment, useContext, useEffect} from 'react'; -import {isInternalFacebookBuild} from 'react-devtools-feature-flags'; -import {ModalDialogContext} from './ModalDialog'; - -export default function DuplicateInstallationDialog(_: {||}) { - const {dispatch} = useContext(ModalDialogContext); - - useEffect(() => { - dispatch({ - canBeDismissed: false, - id: 'DuplicateInstallationDialog', - type: 'SHOW', - title: 'Duplicate Installations of DevTools Detected', - content: , - }); - }, []); - return null; -} - -function DialogContent(_: {||}) { - return ( - -

- We detected that there are multiple versions of React Developer Tools - installed and enabled in your browser at the same time, which will cause - issues while using the extension. -

- {isInternalFacebookBuild ? ( -

- Before proceeding, please ensure that the only enabled version of - React Developer Tools is the internal (Chef-installed) version. To - manage your extensions, visit the about://extensions page - in your browser. -

- ) : ( -

- Please ensure that you have installed and enabled only a single - version of React Developer Tools before proceeding. To manage your - extensions, visit the about://extensions page in your - browser. -

- )} -
- ); -} From c624dc3598c6dc95f9a827667bc4f8ecff41f0bd Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 27 Oct 2021 17:18:48 -0400 Subject: [PATCH 066/109] DevTools supports ENV-injected version for better internal bug reports (#22635) --- packages/react-devtools-extensions/utils.js | 14 ++++++++------ .../react-devtools-extensions/webpack.backend.js | 2 +- .../react-devtools-extensions/webpack.config.js | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/react-devtools-extensions/utils.js b/packages/react-devtools-extensions/utils.js index a096b39b2283c..0cbf23fc68f2a 100644 --- a/packages/react-devtools-extensions/utils.js +++ b/packages/react-devtools-extensions/utils.js @@ -30,12 +30,14 @@ function getGitCommit() { } } -function getVersionString() { - const packageVersion = JSON.parse( - readFileSync( - resolve(__dirname, '..', 'react-devtools-core', './package.json'), - ), - ).version; +function getVersionString(packageVersion = null) { + if (packageVersion == null) { + packageVersion = JSON.parse( + readFileSync( + resolve(__dirname, '..', 'react-devtools-core', './package.json'), + ), + ).version; + } const commit = getGitCommit(); diff --git a/packages/react-devtools-extensions/webpack.backend.js b/packages/react-devtools-extensions/webpack.backend.js index e8c3bded6310e..80198868f1876 100644 --- a/packages/react-devtools-extensions/webpack.backend.js +++ b/packages/react-devtools-extensions/webpack.backend.js @@ -30,7 +30,7 @@ const builtModulesDir = resolve( const __DEV__ = NODE_ENV === 'development'; -const DEVTOOLS_VERSION = getVersionString(); +const DEVTOOLS_VERSION = getVersionString(process.env.DEVTOOLS_VERSION); const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss'; diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index f7ba2f2e76241..99b1d18d3a999 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -30,7 +30,7 @@ const builtModulesDir = resolve( const __DEV__ = NODE_ENV === 'development'; -const DEVTOOLS_VERSION = getVersionString(); +const DEVTOOLS_VERSION = getVersionString(process.env.DEVTOOLS_VERSION); const LOGGING_URL = process.env.LOGGING_URL || null; From 9c8161ba81220143e7f87bd901697e46b14d8968 Mon Sep 17 00:00:00 2001 From: Juan Date: Thu, 28 Oct 2021 11:04:27 -0400 Subject: [PATCH 067/109] Reapply changes from #22631 (#22645) --- packages/react-devtools-extensions/src/backend.js | 6 ++---- packages/react-devtools-extensions/src/main.js | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/react-devtools-extensions/src/backend.js b/packages/react-devtools-extensions/src/backend.js index 5b6821bbba038..a102bed3a90a3 100644 --- a/packages/react-devtools-extensions/src/backend.js +++ b/packages/react-devtools-extensions/src/backend.js @@ -13,16 +13,15 @@ function welcome(event) { ) { return; } - const extensionId = event.data.extensionId; window.removeEventListener('message', welcome); - setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__, extensionId); + setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__); } window.addEventListener('message', welcome); -function setup(hook, extensionId) { +function setup(hook) { if (hook == null) { // DevTools didn't get injected into this page (maybe b'c of the contentType). return; @@ -56,7 +55,6 @@ function setup(hook, extensionId) { { source: 'react-devtools-bridge', payload: {event, payload}, - extensionId, }, '*', transferable, diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index 6a3836839a4ea..70aaf92a7d528 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -27,6 +27,7 @@ const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY = 'React::DevTools::supportsProfiling'; const isChrome = getBrowserName() === 'Chrome'; +const isEdge = getBrowserName() === 'Edge'; let panelCreated = false; @@ -149,7 +150,7 @@ function createPanelIfReactLoaded() { store = new Store(bridge, { isProfiling, - supportsReloadAndProfile: isChrome, + supportsReloadAndProfile: isChrome || isEdge, supportsProfiling, // At this time, the scheduling profiler can only parse Chrome performance profiles. supportsSchedulingProfiler: isChrome, From 7034408ff762b52a39f3a3145a37f4526d0a95cf Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 31 Oct 2021 18:37:32 -0400 Subject: [PATCH 068/109] Follow-up improvements to error code extraction infra (#22516) * Output FIXME during build for unminified errors The invariant Babel transform used to output a FIXME comment if it could not find a matching error code. This could happen if there were a configuration mistake that caused an unminified message to slip through. Linting the compiled bundles is the most reliable way to do it because there's not a one-to-one mapping between source modules and bundles. For example, the same source module may appear in multiple bundles, some which are minified and others which aren't. This updates the transform to output the same messages for Error calls. The source lint rule is still useful for catching mistakes during development, to prompt you to update the error codes map before pushing the PR to CI. * Don't run error transform in development We used to run the error transform in both production and development, because in development it was used to convert `invariant` calls into throw statements. Now that don't use `invariant` anymore, we only have to run the transform for production builds. * Add ! to FIXME comment so Closure doesn't strip it Don't love this solution because Closure could change this heuristic, or we could switch to a differnt compiler that doesn't support it. But it works. Could add a bundle that contains an unminified error solely for the purpose of testing it, but that seems like overkill. * Alternate extract-errors that scrapes artifacts The build script outputs a special FIXME comment when it fails to minify an error message. CI will detect these comments and fail the workflow. The comments also include the expected error message. So I added an alternate extract-errors that scrapes unminified messages from the build artifacts and updates `codes.json`. This is nice because it works on partial builds. And you can also run it after the fact, instead of needing build all over again. * Disable error minification in more bundles Not worth it because the number of errors does not outweight the size of the formatProdErrorMessage runtime. * Run extract-errors script in CI The lint_build job already checks for unminified errors, but the output isn't super helpful. Instead I've added a new job that runs the extract-errors script and fails the build if `codes.json` changes. It also outputs the expected diff so you can easily see which messages were missing from the map. * Replace old extract-errors script with new one Deletes the old extract-errors in favor of extract-errors2 --- .circleci/config.yml | 18 +- package.json | 2 +- .../__tests__/ReactError-test.internal.js | 1 + scripts/error-codes/README.md | 5 +- .../transform-error-messages.js.snap | 79 ++----- .../__tests__/transform-error-messages.js | 83 ++----- scripts/error-codes/extract-errors.js | 143 +++++------- .../error-codes/transform-error-messages.js | 217 ++++-------------- scripts/jest/preprocessor.js | 6 - scripts/rollup/build.js | 50 +--- scripts/rollup/bundles.js | 14 +- 11 files changed, 179 insertions(+), 439 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7d1eb1e8b3381..8c847fc05f39e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -209,7 +209,20 @@ jobs: - run: yarn workspaces info | head -n -1 > workspace_info.txt - *restore_node_modules - run: yarn lint-build - - run: scripts/circleci/check_minified_errors.sh + + check_error_codes: + docker: *docker + environment: *environment + steps: + - checkout + - attach_workspace: *attach_workspace + - run: yarn workspaces info | head -n -1 > workspace_info.txt + - *restore_node_modules + - run: + name: Search build artifacts for unminified errors + command: | + yarn extract-errors + git diff || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false) yarn_test: docker: *docker @@ -414,6 +427,9 @@ workflows: - yarn_lint_build: requires: - yarn_build_combined + - check_error_codes: + requires: + - yarn_build_combined - RELEASE_CHANNEL_stable_yarn_test_dom_fixtures: requires: - yarn_build_combined diff --git a/package.json b/package.json index e7471a686ce88..e4ea1c83be801 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "linc": "node ./scripts/tasks/linc.js", "lint": "node ./scripts/tasks/eslint.js", "lint-build": "node ./scripts/rollup/validate/index.js", - "extract-errors": "yarn build --type=dev --extract-errors", + "extract-errors": "node scripts/error-codes/extract-errors.js", "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js && node ./scripts/yarn/downloadReactIsForPrettyFormat.js", "debug-test": "yarn test --deprecated 'yarn test --debug'", "test": "node ./scripts/jest/jest-cli.js", diff --git a/packages/shared/__tests__/ReactError-test.internal.js b/packages/shared/__tests__/ReactError-test.internal.js index c40e62c6f391e..5576c72bf683e 100644 --- a/packages/shared/__tests__/ReactError-test.internal.js +++ b/packages/shared/__tests__/ReactError-test.internal.js @@ -37,6 +37,7 @@ describe('ReactError', () => { }); // @gate build === "production" + // @gate !source it('should error with minified error code', () => { expect(() => ReactDOM.render('Hi', null)).toThrowError( 'Minified React error #200; visit ' + diff --git a/scripts/error-codes/README.md b/scripts/error-codes/README.md index 9933e9903d1ea..38918bd42a52e 100644 --- a/scripts/error-codes/README.md +++ b/scripts/error-codes/README.md @@ -9,7 +9,10 @@ provide a better debugging support in production. Check out the blog post the file will never be changed/removed. - [`extract-errors.js`](https://github.com/facebook/react/blob/main/scripts/error-codes/extract-errors.js) is an node script that traverses our codebase and updates `codes.json`. You - can test it by running `yarn extract-errors`. + can test it by running `yarn extract-errors`. It works by crawling the build + artifacts directory, so you need to have either run the build script or + downloaded pre-built artifacts (e.g. with `yarn download build`). It works + with partial builds, too. - [`transform-error-messages`](https://github.com/facebook/react/blob/main/scripts/error-codes/transform-error-messages.js) is a Babel pass that rewrites error messages to IDs for a production (minified) build. diff --git a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap index bfb80ab375562..97870a4b31b8f 100644 --- a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap +++ b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap @@ -2,94 +2,47 @@ exports[`error transform handles escaped backticks in template string 1`] = ` "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; -Error(__DEV__ ? \\"Expected \`\\" + listener + \\"\` listener to be a function, instead got a value of \`\\" + type + \\"\` type.\\" : _formatProdErrorMessage(231, listener, type));" +Error(_formatProdErrorMessage(231, listener, type));" `; -exports[`error transform should correctly transform invariants that are not in the error codes map 1`] = ` -"import invariant from 'shared/invariant'; - -/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/ -if (!condition) { - throw Error(\\"This is not a real error message.\\"); -}" +exports[`error transform should not touch other calls or new expressions 1`] = ` +"new NotAnError(); +NotAnError();" `; -exports[`error transform should handle escaped characters 1`] = ` -"import invariant from 'shared/invariant'; +exports[`error transform should output FIXME for errors that don't have a matching error code 1`] = ` +"/*! FIXME (minify-errors-in-prod): Unminified error message in production build!*/ -/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/ -if (!condition) { - throw Error(\\"What's up?\\"); -}" +/*! \\"This is not a real error message.\\"*/ +Error('This is not a real error message.');" `; -exports[`error transform should not touch other calls or new expressions 1`] = ` -"new NotAnError(); -NotAnError();" +exports[`error transform should output FIXME for errors that don't have a matching error code, unless opted out with a comment 1`] = ` +"// eslint-disable-next-line react-internal/prod-error-codes +Error('This is not a real error message.');" `; exports[`error transform should replace error constructors (no new) 1`] = ` "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; -Error(__DEV__ ? 'Do not override existing functions.' : _formatProdErrorMessage(16));" +Error(_formatProdErrorMessage(16));" `; exports[`error transform should replace error constructors 1`] = ` "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; -Error(__DEV__ ? 'Do not override existing functions.' : _formatProdErrorMessage(16));" -`; - -exports[`error transform should replace simple invariant calls 1`] = ` -"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; -import invariant from 'shared/invariant'; - -if (!condition) { - { - throw Error(__DEV__ ? \\"Do not override existing functions.\\" : _formatProdErrorMessage(16)); - } -}" +Error(_formatProdErrorMessage(16));" `; exports[`error transform should support error constructors with concatenated messages 1`] = ` "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; -Error(__DEV__ ? \\"Expected \\" + foo + \\" target to \\" + (\\"be an array; got \\" + bar) : _formatProdErrorMessage(7, foo, bar));" +Error(_formatProdErrorMessage(7, foo, bar));" `; exports[`error transform should support interpolating arguments with concatenation 1`] = ` "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; -Error(__DEV__ ? 'Expected ' + foo + ' target to be an array; got ' + bar : _formatProdErrorMessage(7, foo, bar));" +Error(_formatProdErrorMessage(7, foo, bar));" `; exports[`error transform should support interpolating arguments with template strings 1`] = ` "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; -Error(__DEV__ ? \\"Expected \\" + foo + \\" target to be an array; got \\" + bar : _formatProdErrorMessage(7, foo, bar));" -`; - -exports[`error transform should support invariant calls with a concatenated template string and args 1`] = ` -"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; -import invariant from 'shared/invariant'; - -if (!condition) { - { - throw Error(__DEV__ ? \\"Expected a component class, got \\" + Foo + \\".\\" + Bar : _formatProdErrorMessage(18, Foo, Bar)); - } -}" -`; - -exports[`error transform should support invariant calls with args 1`] = ` -"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; -import invariant from 'shared/invariant'; - -if (!condition) { - { - throw Error(__DEV__ ? \\"Expected \\" + foo + \\" target to be an array; got \\" + bar : _formatProdErrorMessage(7, foo, bar)); - } -}" -`; - -exports[`error transform should support noMinify option 1`] = ` -"import invariant from 'shared/invariant'; - -if (!condition) { - throw Error(\\"Do not override existing functions.\\"); -}" +Error(_formatProdErrorMessage(7, foo, bar));" `; diff --git a/scripts/error-codes/__tests__/transform-error-messages.js b/scripts/error-codes/__tests__/transform-error-messages.js index 2bca7366321e2..3cf08b69e848f 100644 --- a/scripts/error-codes/__tests__/transform-error-messages.js +++ b/scripts/error-codes/__tests__/transform-error-messages.js @@ -28,87 +28,46 @@ describe('error transform', () => { process.env.NODE_ENV = oldEnv; }); - it('should replace simple invariant calls', () => { - expect( - transform(` -import invariant from 'shared/invariant'; -invariant(condition, 'Do not override existing functions.'); -`) - ).toMatchSnapshot(); - }); - - it('should throw if invariant is not in an expression statement', () => { - expect(() => { - transform(` -import invariant from 'shared/invariant'; -cond && invariant(condition, 'Do not override existing functions.'); -`); - }).toThrow('invariant() cannot be called from expression context'); - }); - - it('should support invariant calls with args', () => { - expect( - transform(` -import invariant from 'shared/invariant'; -invariant(condition, 'Expected %s target to be an array; got %s', foo, bar); -`) - ).toMatchSnapshot(); - }); - - it('should support invariant calls with a concatenated template string and args', () => { - expect( - transform(` -import invariant from 'shared/invariant'; -invariant(condition, 'Expected a component class, ' + 'got %s.' + '%s', Foo, Bar); -`) - ).toMatchSnapshot(); - }); - - it('should correctly transform invariants that are not in the error codes map', () => { + it('should replace error constructors', () => { expect( transform(` -import invariant from 'shared/invariant'; -invariant(condition, 'This is not a real error message.'); +new Error('Do not override existing functions.'); `) ).toMatchSnapshot(); }); - it('should handle escaped characters', () => { + it('should replace error constructors (no new)', () => { expect( transform(` -import invariant from 'shared/invariant'; -invariant(condition, 'What\\'s up?'); +Error('Do not override existing functions.'); `) ).toMatchSnapshot(); }); - it('should support noMinify option', () => { - expect( - transform( - ` -import invariant from 'shared/invariant'; -invariant(condition, 'Do not override existing functions.'); -`, - {noMinify: true} - ) - ).toMatchSnapshot(); - }); - - it('should replace error constructors', () => { + it("should output FIXME for errors that don't have a matching error code", () => { expect( transform(` -new Error('Do not override existing functions.'); +Error('This is not a real error message.'); `) ).toMatchSnapshot(); }); - it('should replace error constructors (no new)', () => { - expect( - transform(` -Error('Do not override existing functions.'); + it( + "should output FIXME for errors that don't have a matching error " + + 'code, unless opted out with a comment', + () => { + // TODO: Since this only detects one of many ways to disable a lint + // rule, we should instead search for a custom directive (like + // no-minify-errors) instead of ESLint. Will need to update our lint + // rule to recognize the same directive. + expect( + transform(` +// eslint-disable-next-line react-internal/prod-error-codes +Error('This is not a real error message.'); `) - ).toMatchSnapshot(); - }); + ).toMatchSnapshot(); + } + ); it('should not touch other calls or new expressions', () => { expect( diff --git a/scripts/error-codes/extract-errors.js b/scripts/error-codes/extract-errors.js index d60ffe308cdbe..addb095b4ca1f 100644 --- a/scripts/error-codes/extract-errors.js +++ b/scripts/error-codes/extract-errors.js @@ -1,105 +1,74 @@ -/** - * 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. - */ 'use strict'; -const parser = require('@babel/parser'); const fs = require('fs'); const path = require('path'); -const traverse = require('@babel/traverse').default; -const {evalStringConcat} = require('../shared/evalToString'); -const invertObject = require('./invertObject'); +const {execSync} = require('child_process'); -const babylonOptions = { - sourceType: 'module', - // As a parser, babylon has its own options and we can't directly - // import/require a babel preset. It should be kept **the same** as - // the `babel-plugin-syntax-*` ones specified in - // https://github.com/facebook/fbjs/blob/master/packages/babel-preset-fbjs/configure.js - plugins: [ - 'classProperties', - 'flow', - 'jsx', - 'trailingFunctionCommas', - 'objectRestSpread', - ], -}; - -module.exports = function(opts) { - if (!opts || !('errorMapFilePath' in opts)) { - throw new Error( - 'Missing options. Ensure you pass an object with `errorMapFilePath`.' - ); +async function main() { + const originalJSON = JSON.parse( + fs.readFileSync(path.resolve(__dirname, '../error-codes/codes.json')) + ); + const existingMessages = new Set(); + const codes = Object.keys(originalJSON); + let nextCode = 0; + for (let i = 0; i < codes.length; i++) { + const codeStr = codes[i]; + const message = originalJSON[codeStr]; + const code = parseInt(codeStr, 10); + existingMessages.add(message); + if (code >= nextCode) { + nextCode = code + 1; + } } - const errorMapFilePath = opts.errorMapFilePath; - let existingErrorMap; + console.log('Searching `build` directory for unminified errors...\n'); + + let out; try { - // Using `fs.readFileSync` instead of `require` here, because `require()` - // calls are cached, and the cache map is not properly invalidated after - // file changes. - existingErrorMap = JSON.parse( - fs.readFileSync( - path.join(__dirname, path.basename(errorMapFilePath)), - 'utf8' - ) - ); + out = execSync( + "git --no-pager grep -n --untracked --no-exclude-standard '/*! ' -- build" + ).toString(); } catch (e) { - existingErrorMap = {}; - } - - const allErrorIDs = Object.keys(existingErrorMap); - let currentID; - - if (allErrorIDs.length === 0) { - // Map is empty - currentID = 0; - } else { - currentID = Math.max.apply(null, allErrorIDs) + 1; + if (e.status === 1 && e.stdout.toString() === '') { + // No unminified errors found. + return; + } + throw e; } - // Here we invert the map object in memory for faster error code lookup - existingErrorMap = invertObject(existingErrorMap); - - function transform(source) { - const ast = parser.parse(source, babylonOptions); - - traverse(ast, { - CallExpression: { - exit(astPath) { - if (astPath.get('callee').isIdentifier({name: 'invariant'})) { - const node = astPath.node; + let newJSON = null; + const regex = /\"(.+?)"\<\/expected-error-format\>/g; + do { + const match = regex.exec(out); + if (match === null) { + break; + } else { + const message = match[1].trim(); + if (existingMessages.has(message)) { + // This probably means you ran the script twice. + continue; + } + existingMessages.add(message); - // error messages can be concatenated (`+`) at runtime, so here's a - // trivial partial evaluator that interprets the literal value - const errorMsgLiteral = evalStringConcat(node.arguments[1]); - addToErrorMap(errorMsgLiteral); - } - }, - }, - }); - } - - function addToErrorMap(errorMsgLiteral) { - if (existingErrorMap.hasOwnProperty(errorMsgLiteral)) { - return; + // Add to json map + if (newJSON === null) { + newJSON = Object.assign({}, originalJSON); + } + console.log(`"${nextCode}": "${message}"`); + newJSON[nextCode] = message; + nextCode += 1; } - existingErrorMap[errorMsgLiteral] = '' + currentID++; - } + } while (true); - function flush(cb) { + if (newJSON) { fs.writeFileSync( - errorMapFilePath, - JSON.stringify(invertObject(existingErrorMap), null, 2) + '\n', - 'utf-8' + path.resolve(__dirname, '../error-codes/codes.json'), + JSON.stringify(newJSON, null, 2) ); } +} - return function extractErrors(source) { - transform(source); - flush(); - }; -}; +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/error-codes/transform-error-messages.js b/scripts/error-codes/transform-error-messages.js index 2baf4baa1c1a7..a429ed4008b68 100644 --- a/scripts/error-codes/transform-error-messages.js +++ b/scripts/error-codes/transform-error-messages.js @@ -7,10 +7,7 @@ 'use strict'; const fs = require('fs'); -const { - evalStringConcat, - evalStringAndTemplateConcat, -} = require('../shared/evalToString'); +const {evalStringAndTemplateConcat} = require('../shared/evalToString'); const invertObject = require('./invertObject'); const helperModuleImports = require('@babel/helper-module-imports'); @@ -23,11 +20,7 @@ const SEEN_SYMBOL = Symbol('transform-error-messages.seen'); module.exports = function(babel) { const t = babel.types; - // TODO: Instead of outputting __DEV__ conditions, only apply this transform - // in production. - const DEV_EXPRESSION = t.identifier('__DEV__'); - - function CallOrNewExpression(path, file) { + function ErrorCallExpression(path, file) { // Turns this code: // // new Error(`A ${adj} message that contains ${noun}`); @@ -38,11 +31,7 @@ module.exports = function(babel) { // // into this: // - // Error( - // __DEV__ - // ? `A ${adj} message that contains ${noun}` - // : formatProdErrorMessage(ERR_CODE, adj, noun) - // ); + // Error(formatProdErrorMessage(ERR_CODE, adj, noun)); const node = path.node; if (node[SEEN_SYMBOL]) { return; @@ -62,9 +51,44 @@ module.exports = function(babel) { let prodErrorId = errorMap[errorMsgLiteral]; if (prodErrorId === undefined) { - // There is no error code for this message. We use a lint rule to - // enforce that messages can be minified, so assume this is - // intentional and exit gracefully. + // There is no error code for this message. Add an inline comment + // that flags this as an unminified error. This allows the build + // to proceed, while also allowing a post-build linter to detect it. + // + // Outputs: + // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ + // /* "A % message that contains %" */ + // if (!condition) { + // throw Error(`A ${adj} message that contains ${noun}`); + // } + + const statementParent = path.getStatementParent(); + const leadingComments = statementParent.node.leadingComments; + if (leadingComments !== undefined) { + for (let i = 0; i < leadingComments.length; i++) { + // TODO: Since this only detects one of many ways to disable a lint + // rule, we should instead search for a custom directive (like + // no-minify-errors) instead of ESLint. Will need to update our lint + // rule to recognize the same directive. + const commentText = leadingComments[i].value; + if ( + commentText.includes( + 'eslint-disable-next-line react-internal/prod-error-codes' + ) + ) { + return; + } + } + } + + statementParent.addComment( + 'leading', + `! "${errorMsgLiteral}"` + ); + statementParent.addComment( + 'leading', + '! FIXME (minify-errors-in-prod): Unminified error message in production build!' + ); return; } prodErrorId = parseInt(prodErrorId, 10); @@ -84,168 +108,25 @@ module.exports = function(babel) { ]); // Outputs: - // Error( - // __DEV__ - // ? `A ${adj} message that contains ${noun}` - // : formatProdErrorMessage(ERR_CODE, adj, noun) - // ); - path.replaceWith(t.callExpression(t.identifier('Error'), [prodMessage])); - path.replaceWith( - t.callExpression(t.identifier('Error'), [ - t.conditionalExpression(DEV_EXPRESSION, errorMsgNode, prodMessage), - ]) - ); + // Error(formatProdErrorMessage(ERR_CODE, adj, noun)); + const newErrorCall = t.callExpression(t.identifier('Error'), [prodMessage]); + newErrorCall[SEEN_SYMBOL] = true; + path.replaceWith(newErrorCall); } return { visitor: { NewExpression(path, file) { - const noMinify = file.opts.noMinify; - if (!noMinify && path.get('callee').isIdentifier({name: 'Error'})) { - CallOrNewExpression(path, file); + if (path.get('callee').isIdentifier({name: 'Error'})) { + ErrorCallExpression(path, file); } }, CallExpression(path, file) { - const node = path.node; - const noMinify = file.opts.noMinify; - - if (!noMinify && path.get('callee').isIdentifier({name: 'Error'})) { - CallOrNewExpression(path, file); + if (path.get('callee').isIdentifier({name: 'Error'})) { + ErrorCallExpression(path, file); return; } - - if (path.get('callee').isIdentifier({name: 'invariant'})) { - // Turns this code: - // - // invariant(condition, 'A %s message that contains %s', adj, noun); - // - // into this: - // - // if (!condition) { - // throw Error( - // __DEV__ - // ? `A ${adj} message that contains ${noun}` - // : formatProdErrorMessage(ERR_CODE, adj, noun) - // ); - // } - // - // where ERR_CODE is an error code: a unique identifier (a number - // string) that references a verbose error message. The mapping is - // stored in `scripts/error-codes/codes.json`. - const condition = node.arguments[0]; - const errorMsgLiteral = evalStringConcat(node.arguments[1]); - const errorMsgExpressions = Array.from(node.arguments.slice(2)); - const errorMsgQuasis = errorMsgLiteral - .split('%s') - .map(raw => t.templateElement({raw, cooked: String.raw({raw})})); - - // Outputs: - // `A ${adj} message that contains ${noun}`; - const devMessage = t.templateLiteral( - errorMsgQuasis, - errorMsgExpressions - ); - - const parentStatementPath = path.parentPath; - if (parentStatementPath.type !== 'ExpressionStatement') { - throw path.buildCodeFrameError( - 'invariant() cannot be called from expression context. Move ' + - 'the call to its own statement.' - ); - } - - if (noMinify) { - // Error minification is disabled for this build. - // - // Outputs: - // if (!condition) { - // throw Error(`A ${adj} message that contains ${noun}`); - // } - parentStatementPath.replaceWith( - t.ifStatement( - t.unaryExpression('!', condition), - t.blockStatement([ - t.throwStatement( - t.callExpression(t.identifier('Error'), [devMessage]) - ), - ]) - ) - ); - return; - } - - let prodErrorId = errorMap[errorMsgLiteral]; - - if (prodErrorId === undefined) { - // There is no error code for this message. Add an inline comment - // that flags this as an unminified error. This allows the build - // to proceed, while also allowing a post-build linter to detect it. - // - // Outputs: - // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ - // if (!condition) { - // throw Error(`A ${adj} message that contains ${noun}`); - // } - parentStatementPath.replaceWith( - t.ifStatement( - t.unaryExpression('!', condition), - t.blockStatement([ - t.throwStatement( - t.callExpression(t.identifier('Error'), [devMessage]) - ), - ]) - ) - ); - parentStatementPath.addComment( - 'leading', - 'FIXME (minify-errors-in-prod): Unminified error message in production build!' - ); - return; - } - prodErrorId = parseInt(prodErrorId, 10); - - // Import formatProdErrorMessage - const formatProdErrorMessageIdentifier = helperModuleImports.addDefault( - path, - 'shared/formatProdErrorMessage', - {nameHint: 'formatProdErrorMessage'} - ); - - // Outputs: - // formatProdErrorMessage(ERR_CODE, adj, noun); - const prodMessage = t.callExpression( - formatProdErrorMessageIdentifier, - [t.numericLiteral(prodErrorId), ...errorMsgExpressions] - ); - - // Outputs: - // if (!condition) { - // throw Error( - // __DEV__ - // ? `A ${adj} message that contains ${noun}` - // : formatProdErrorMessage(ERR_CODE, adj, noun) - // ); - // } - parentStatementPath.replaceWith( - t.ifStatement( - t.unaryExpression('!', condition), - t.blockStatement([ - t.blockStatement([ - t.throwStatement( - t.callExpression(t.identifier('Error'), [ - t.conditionalExpression( - DEV_EXPRESSION, - devMessage, - prodMessage - ), - ]) - ), - ]), - ]) - ) - ); - } }, }, }; diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js index 072071be07fbd..d7a5a2cdab6da 100644 --- a/scripts/jest/preprocessor.js +++ b/scripts/jest/preprocessor.js @@ -13,9 +13,6 @@ const pathToBabel = path.join( '../..', 'package.json' ); -const pathToBabelPluginDevWithCode = require.resolve( - '../error-codes/transform-error-messages' -); const pathToBabelPluginReplaceConsoleCalls = require.resolve( '../babel/transform-replace-console-calls' ); @@ -36,8 +33,6 @@ const babelOptions = { // For Node environment only. For builds, Rollup takes care of ESM. require.resolve('@babel/plugin-transform-modules-commonjs'), - pathToBabelPluginDevWithCode, - // Keep stacks detailed in tests. // Don't put this in .babelrc so that we don't embed filenames // into ReactART builds that include JSX. @@ -105,7 +100,6 @@ module.exports = { __filename, pathToBabel, pathToBabelrc, - pathToBabelPluginDevWithCode, pathToTransformInfiniteLoops, pathToTransformTestGatePragma, pathToErrorCodes, diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 7e29c0e8c54d3..48401c002b431 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -19,7 +19,6 @@ const Sync = require('./sync'); const sizes = require('./plugins/sizes-plugin'); const useForks = require('./plugins/use-forks-plugin'); const stripUnusedImports = require('./plugins/strip-unused-imports'); -const extractErrorCodes = require('../error-codes/extract-errors'); const Packaging = require('./packaging'); const {asyncRimRaf} = require('./utils'); const codeFrame = require('babel-code-frame'); @@ -94,10 +93,6 @@ const forcePrettyOutput = argv.pretty; const isWatchMode = argv.watch; const syncFBSourcePath = argv['sync-fbsource']; const syncWWWPath = argv['sync-www']; -const shouldExtractErrors = argv['extract-errors']; -const errorCodeOpts = { - errorMapFilePath: 'scripts/error-codes/codes.json', -}; const closureOptions = { compilation_level: 'SIMPLE', @@ -176,26 +171,13 @@ function getBabelConfig( if (updateBabelOptions) { options = updateBabelOptions(options); } + // Controls whether to replace error messages with error codes in production. + // By default, error messages are replaced in production. + if (!isDevelopment && bundle.minifyWithProdErrorCodes !== false) { + options.plugins.push(require('../error-codes/transform-error-messages')); + } + switch (bundleType) { - case FB_WWW_DEV: - case FB_WWW_PROD: - case FB_WWW_PROFILING: - case RN_OSS_DEV: - case RN_OSS_PROD: - case RN_OSS_PROFILING: - case RN_FB_DEV: - case RN_FB_PROD: - case RN_FB_PROFILING: - return Object.assign({}, options, { - plugins: options.plugins.concat([ - [ - require('../error-codes/transform-error-messages'), - // Controls whether to replace error messages with error codes - // in production. By default, error messages are replaced. - {noMinify: bundle.minifyWithProdErrorCodes === false}, - ], - ]), - }); case UMD_DEV: case UMD_PROD: case UMD_PROFILING: @@ -206,8 +188,6 @@ function getBabelConfig( plugins: options.plugins.concat([ // Use object-assign polyfill in open source path.resolve('./scripts/babel/transform-object-assign-require'), - // Minify invariant messages - require('../error-codes/transform-error-messages'), ]), }); default: @@ -339,7 +319,6 @@ function getPlugins( pureExternalModules, bundle ) { - const findAndRecordErrorCodes = extractErrorCodes(errorCodeOpts); const forks = Modules.getForks(bundleType, entry, moduleType, bundle); const isProduction = isProductionBundleType(bundleType); const isProfiling = isProfilingBundleType(bundleType); @@ -360,13 +339,6 @@ function getPlugins( bundleType === RN_FB_PROFILING; const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput; return [ - // Extract error codes from invariant() messages into a file. - shouldExtractErrors && { - transform(source) { - findAndRecordErrorCodes(source); - return source; - }, - }, // Shim any modules that need forking in this environment. useForks(forks), // Ensure we don't try to bundle any fbjs modules. @@ -762,7 +734,7 @@ async function buildEverything() { ); } - if (!shouldExtractErrors && process.env.CIRCLE_NODE_TOTAL) { + if (process.env.CIRCLE_NODE_TOTAL) { // In CI, parallelize bundles across multiple tasks. const nodeTotal = parseInt(process.env.CIRCLE_NODE_TOTAL, 10); const nodeIndex = parseInt(process.env.CIRCLE_NODE_INDEX, 10); @@ -787,14 +759,6 @@ async function buildEverything() { if (!forcePrettyOutput) { Stats.saveResults(); } - - if (shouldExtractErrors) { - console.warn( - '\nWarning: this build was created with --extract-errors enabled.\n' + - 'this will result in extremely slow builds and should only be\n' + - 'used when the error map needs to be rebuilt.\n' - ); - } } buildEverything(); diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index af7b30b7feb99..40ae43ae7cbc0 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -141,7 +141,7 @@ const bundles = [ moduleType: ISOMORPHIC, entry: 'react-fetch/index.browser', global: 'ReactFetch', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -163,7 +163,7 @@ const bundles = [ moduleType: ISOMORPHIC, entry: 'react-fs/index.browser.server', global: 'ReactFilesystem', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: [], }, @@ -185,7 +185,7 @@ const bundles = [ moduleType: ISOMORPHIC, entry: 'react-pg/index.browser.server', global: 'ReactPostgres', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: [], }, @@ -349,7 +349,7 @@ const bundles = [ moduleType: RENDERER, entry: 'react-server-dom-webpack', global: 'ReactServerDOMReader', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -594,7 +594,7 @@ const bundles = [ moduleType: RENDERER, entry: 'react-noop-renderer', global: 'ReactNoopRenderer', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: ['react', 'scheduler', 'scheduler/unstable_mock', 'expect'], }, @@ -605,7 +605,7 @@ const bundles = [ moduleType: RENDERER, entry: 'react-noop-renderer/persistent', global: 'ReactNoopRendererPersistent', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: ['react', 'scheduler', 'expect'], }, @@ -616,7 +616,7 @@ const bundles = [ moduleType: RENDERER, entry: 'react-noop-renderer/server', global: 'ReactNoopRendererServer', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: ['react', 'scheduler', 'expect'], }, From 6bce0355c3e4bf23c16e82317094230908ee7560 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 31 Oct 2021 18:38:03 -0400 Subject: [PATCH 069/109] Upgrade useSyncExternalStore to alpha channel (#22662) * Move useSyncExternalStore shim to a nested entrypoint Also renames `useSyncExternalStoreExtra` to `useSyncExternalStoreWithSelector`. - 'use-sync-external-store/shim' -> A shim for `useSyncExternalStore` that works in React 16 and 17 (any release that supports hooks). The module will first check if the built-in React API exists, before falling back to the shim. - 'use-sync-external-store/with-selector' -> An extended version of `useSyncExternalStore` that also supports `selector` and `isEqual` options. It does _not_ shim `use-sync-external-store`; it composes the built-in React API. **Use this if you only support 18+.** - 'use-sync-external-store/shim/with-selector' -> Same API, but it composes `use-sync-external-store/shim` instead. **Use this for compatibility with 16 and 17.** - 'use-sync-external-store' -> Re-exports React's built-in API. Not meant to be used. It will warn and direct users to either the shim or the built-in API. * Upgrade useSyncExternalStore to alpha channel --- ReactVersions.js | 2 +- .../ReactHooksInspectionIntegration-test.js | 3 +- .../src/__tests__/ReactDOMFizzServer-test.js | 26 ++++++--- .../__tests__/useSyncExternalStore-test.js | 3 +- packages/react/index.classic.fb.js | 1 - packages/react/index.experimental.js | 2 +- packages/react/index.js | 1 - packages/react/index.modern.fb.js | 1 - packages/react/index.stable.js | 1 + packages/use-sync-external-store/index.js | 2 +- packages/use-sync-external-store/npm/extra.js | 7 --- .../npm/index.native.js | 7 --- .../use-sync-external-store/npm/shim/index.js | 7 +++ .../npm/shim/index.native.js | 7 +++ .../npm/shim/with-selector.js | 7 +++ .../npm/with-selector.js | 7 +++ packages/use-sync-external-store/package.json | 4 +- .../use-sync-external-store/shim/index.js | 12 ++++ .../{ => shim}/index.native.js | 2 +- .../shim/with-selector/index.js | 12 ++++ .../useSyncExternalStoreNative-test.js | 55 +++++++++---------- .../useSyncExternalStoreShared-test.js | 47 +++++++++------- .../useSyncExternalStoreShimServer-test.js | 4 +- .../forks/isServerEnvironment.native.js} | 4 +- ...seSyncExternalStore.forward-to-built-in.js | 16 ++++++ .../useSyncExternalStore.forward-to-shim.js | 16 ++++++ .../src/isServerEnvironment.js | 12 ++++ .../src/useSyncExternalStore.js | 28 ++++++---- .../src/useSyncExternalStoreShim.js | 18 ++++++ ...t.js => useSyncExternalStoreShimClient.js} | 8 +-- ...r.js => useSyncExternalStoreShimServer.js} | 4 ++ ...js => useSyncExternalStoreWithSelector.js} | 8 +-- .../use-sync-external-store/with-selector.js | 12 ++++ scripts/jest/TestFlags.js | 5 +- scripts/jest/config.build.js | 7 +++ scripts/rollup/bundles.js | 44 +++++++++++---- scripts/rollup/forks.js | 18 ++++++ scripts/shared/pathsByLanguageVersion.js | 2 + 38 files changed, 298 insertions(+), 124 deletions(-) delete mode 100644 packages/use-sync-external-store/npm/extra.js delete mode 100644 packages/use-sync-external-store/npm/index.native.js create mode 100644 packages/use-sync-external-store/npm/shim/index.js create mode 100644 packages/use-sync-external-store/npm/shim/index.native.js create mode 100644 packages/use-sync-external-store/npm/shim/with-selector.js create mode 100644 packages/use-sync-external-store/npm/with-selector.js create mode 100644 packages/use-sync-external-store/shim/index.js rename packages/use-sync-external-store/{ => shim}/index.native.js (70%) create mode 100644 packages/use-sync-external-store/shim/with-selector/index.js rename packages/use-sync-external-store/{extra.js => src/forks/isServerEnvironment.native.js} (75%) create mode 100644 packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-built-in.js create mode 100644 packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-shim.js create mode 100644 packages/use-sync-external-store/src/isServerEnvironment.js create mode 100644 packages/use-sync-external-store/src/useSyncExternalStoreShim.js rename packages/use-sync-external-store/src/{useSyncExternalStoreClient.js => useSyncExternalStoreShimClient.js} (95%) rename packages/use-sync-external-store/src/{useSyncExternalStoreServer.js => useSyncExternalStoreShimServer.js} (59%) rename packages/use-sync-external-store/src/{useSyncExternalStoreExtra.js => useSyncExternalStoreWithSelector.js} (95%) create mode 100644 packages/use-sync-external-store/with-selector.js diff --git a/ReactVersions.js b/ReactVersions.js index 2ce7680f0191c..bdab35424e885 100644 --- a/ReactVersions.js +++ b/ReactVersions.js @@ -36,6 +36,7 @@ const stablePackages = { 'react-refresh': '0.11.0', 'react-test-renderer': ReactVersion, 'use-subscription': '1.6.0', + 'use-sync-external-store': '1.0.0', scheduler: '0.21.0', }; @@ -47,7 +48,6 @@ const experimentalPackages = [ 'react-fs', 'react-pg', 'react-server-dom-webpack', - 'use-sync-external-store', ]; module.exports = { diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 6937efd631572..087d74d628724 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -1055,9 +1055,8 @@ describe('ReactHooksInspectionIntegration', () => { ]); }); - // @gate experimental || www it('should support composite useSyncExternalStore hook', () => { - const useSyncExternalStore = React.unstable_useSyncExternalStore; + const useSyncExternalStore = React.useSyncExternalStore; function Foo() { const value = useSyncExternalStore( () => () => {}, diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index a2ede1746d08a..abbbdc1578753 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -18,7 +18,7 @@ let ReactDOMFizzServer; let Suspense; let SuspenseList; let useSyncExternalStore; -let useSyncExternalStoreExtra; +let useSyncExternalStoreWithSelector; let PropTypes; let textCache; let window; @@ -43,11 +43,23 @@ describe('ReactDOMFizzServer', () => { Stream = require('stream'); Suspense = React.Suspense; SuspenseList = React.SuspenseList; - useSyncExternalStore = React.unstable_useSyncExternalStore; - useSyncExternalStoreExtra = require('use-sync-external-store/extra') - .useSyncExternalStoreExtra; + PropTypes = require('prop-types'); + if (gate(flags => flags.source)) { + // The `with-selector` module composes the main `use-sync-external-store` + // entrypoint. In the compiled artifacts, this is resolved to the `shim` + // implementation by our build config, but when running the tests against + // the source files, we need to tell Jest how to resolve it. Because this + // is a source module, this mock has no affect on the build tests. + jest.mock('use-sync-external-store/src/useSyncExternalStore', () => + jest.requireActual('react'), + ); + } + useSyncExternalStore = React.useSyncExternalStore; + useSyncExternalStoreWithSelector = require('use-sync-external-store/with-selector') + .useSyncExternalStoreWithSelector; + textCache = new Map(); // Test Environment @@ -1663,7 +1675,6 @@ describe('ReactDOMFizzServer', () => { ); }); - // @gate supportsNativeUseSyncExternalStore // @gate experimental it('calls getServerSnapshot instead of getSnapshot', async () => { const ref = React.createRef(); @@ -1734,7 +1745,6 @@ describe('ReactDOMFizzServer', () => { // The selector implementation uses the lazy ref initialization pattern // @gate !(enableUseRefAccessWarning && __DEV__) - // @gate supportsNativeUseSyncExternalStore // @gate experimental it('calls getServerSnapshot instead of getSnapshot (with selector and isEqual)', async () => { // Same as previous test, but with a selector that returns a complex object @@ -1767,7 +1777,7 @@ describe('ReactDOMFizzServer', () => { } function App() { - const {env} = useSyncExternalStoreExtra( + const {env} = useSyncExternalStoreWithSelector( subscribe, getClientSnapshot, getServerSnapshot, @@ -1815,7 +1825,6 @@ describe('ReactDOMFizzServer', () => { expect(ref.current).toEqual(serverRenderedDiv); }); - // @gate supportsNativeUseSyncExternalStore // @gate experimental it( 'errors during hydration force a client render at the nearest Suspense ' + @@ -1964,7 +1973,6 @@ describe('ReactDOMFizzServer', () => { }, ); - // @gate supportsNativeUseSyncExternalStore // @gate experimental it( 'errors during hydration force a client render at the nearest Suspense ' + diff --git a/packages/react-reconciler/src/__tests__/useSyncExternalStore-test.js b/packages/react-reconciler/src/__tests__/useSyncExternalStore-test.js index d939a02bee897..623ea12755907 100644 --- a/packages/react-reconciler/src/__tests__/useSyncExternalStore-test.js +++ b/packages/react-reconciler/src/__tests__/useSyncExternalStore-test.js @@ -36,7 +36,7 @@ describe('useSyncExternalStore', () => { useImperativeHandle = React.useImperativeHandle; forwardRef = React.forwardRef; useRef = React.useRef; - useSyncExternalStore = React.unstable_useSyncExternalStore; + useSyncExternalStore = React.useSyncExternalStore; startTransition = React.startTransition; act = require('jest-react').act; @@ -70,7 +70,6 @@ describe('useSyncExternalStore', () => { }; } - // @gate supportsNativeUseSyncExternalStore test( 'detects interleaved mutations during a concurrent read before ' + 'layout effects fire', diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 568a8ada613d9..c5854f8f6d398 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -54,7 +54,6 @@ export { useMutableSource, useMutableSource as unstable_useMutableSource, useSyncExternalStore, - useSyncExternalStore as unstable_useSyncExternalStore, useReducer, useRef, useState, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index 7491bbb7e832d..4b4fa89e01898 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -47,7 +47,7 @@ export { useLayoutEffect, useMemo, useMutableSource as unstable_useMutableSource, - useSyncExternalStore as unstable_useSyncExternalStore, + useSyncExternalStore, useReducer, useRef, useState, diff --git a/packages/react/index.js b/packages/react/index.js index 59cc05f0254e6..9a6a99ee52189 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -73,7 +73,6 @@ export { useMemo, useMutableSource, useSyncExternalStore, - useSyncExternalStore as unstable_useSyncExternalStore, useReducer, useRef, useState, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index cd60ee426fa65..8d08a43b90946 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -53,7 +53,6 @@ export { useMutableSource, useMutableSource as unstable_useMutableSource, useSyncExternalStore, - useSyncExternalStore as unstable_useSyncExternalStore, useReducer, useRef, useState, diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index 4e4682b8fc29f..3a0600d11a713 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -40,6 +40,7 @@ export { useLayoutEffect, useMemo, useMutableSource as unstable_useMutableSource, + useSyncExternalStore, useReducer, useRef, useState, diff --git a/packages/use-sync-external-store/index.js b/packages/use-sync-external-store/index.js index ff57d66d841bc..55548c1126626 100644 --- a/packages/use-sync-external-store/index.js +++ b/packages/use-sync-external-store/index.js @@ -9,4 +9,4 @@ 'use strict'; -export * from './src/useSyncExternalStore'; +export {useSyncExternalStore} from './src/useSyncExternalStore'; diff --git a/packages/use-sync-external-store/npm/extra.js b/packages/use-sync-external-store/npm/extra.js deleted file mode 100644 index 468dd518ac1fc..0000000000000 --- a/packages/use-sync-external-store/npm/extra.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/use-sync-external-store-extra.production.min.js'); -} else { - module.exports = require('./cjs/use-sync-external-store-extra.development.js'); -} diff --git a/packages/use-sync-external-store/npm/index.native.js b/packages/use-sync-external-store/npm/index.native.js deleted file mode 100644 index 22546b9c0ebba..0000000000000 --- a/packages/use-sync-external-store/npm/index.native.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/use-sync-external-store.native.production.min.js'); -} else { - module.exports = require('./cjs/use-sync-external-store.native.development.js'); -} diff --git a/packages/use-sync-external-store/npm/shim/index.js b/packages/use-sync-external-store/npm/shim/index.js new file mode 100644 index 0000000000000..bde36f38efdb3 --- /dev/null +++ b/packages/use-sync-external-store/npm/shim/index.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('../cjs/use-sync-external-store-shim.production.min.js'); +} else { + module.exports = require('../cjs/use-sync-external-store-shim.development.js'); +} diff --git a/packages/use-sync-external-store/npm/shim/index.native.js b/packages/use-sync-external-store/npm/shim/index.native.js new file mode 100644 index 0000000000000..0bd5c7e1c0f85 --- /dev/null +++ b/packages/use-sync-external-store/npm/shim/index.native.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('../cjs/use-sync-external-store-shim.native.production.min.js'); +} else { + module.exports = require('../cjs/use-sync-external-store-shim.native.development.js'); +} diff --git a/packages/use-sync-external-store/npm/shim/with-selector.js b/packages/use-sync-external-store/npm/shim/with-selector.js new file mode 100644 index 0000000000000..1175186c64286 --- /dev/null +++ b/packages/use-sync-external-store/npm/shim/with-selector.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('../cjs/use-sync-external-store-shim/with-selector.production.min.js'); +} else { + module.exports = require('../cjs/use-sync-external-store-shim/with-selector.development.js'); +} diff --git a/packages/use-sync-external-store/npm/with-selector.js b/packages/use-sync-external-store/npm/with-selector.js new file mode 100644 index 0000000000000..9163b3e7e13f7 --- /dev/null +++ b/packages/use-sync-external-store/npm/with-selector.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/use-sync-external-store-with-selector.production.min.js'); +} else { + module.exports = require('./cjs/use-sync-external-store-with-selector.development.js'); +} diff --git a/packages/use-sync-external-store/package.json b/packages/use-sync-external-store/package.json index b43b3a0ec67d2..0d1d9b8d25f80 100644 --- a/packages/use-sync-external-store/package.json +++ b/packages/use-sync-external-store/package.json @@ -12,8 +12,10 @@ "README.md", "build-info.json", "index.js", - "extra.js", "index.native.js", + "with-selector.js", + "with-selector.native.js", + "shim/", "cjs/" ], "license": "MIT", diff --git a/packages/use-sync-external-store/shim/index.js b/packages/use-sync-external-store/shim/index.js new file mode 100644 index 0000000000000..b8fa72b48e775 --- /dev/null +++ b/packages/use-sync-external-store/shim/index.js @@ -0,0 +1,12 @@ +/** + * 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 + */ + +'use strict'; + +export {useSyncExternalStore} from 'use-sync-external-store/src/useSyncExternalStoreShim'; diff --git a/packages/use-sync-external-store/index.native.js b/packages/use-sync-external-store/shim/index.native.js similarity index 70% rename from packages/use-sync-external-store/index.native.js rename to packages/use-sync-external-store/shim/index.native.js index cac5c1c2bf710..b8fa72b48e775 100644 --- a/packages/use-sync-external-store/index.native.js +++ b/packages/use-sync-external-store/shim/index.native.js @@ -9,4 +9,4 @@ 'use strict'; -export * from './src/useSyncExternalStoreClient'; +export {useSyncExternalStore} from 'use-sync-external-store/src/useSyncExternalStoreShim'; diff --git a/packages/use-sync-external-store/shim/with-selector/index.js b/packages/use-sync-external-store/shim/with-selector/index.js new file mode 100644 index 0000000000000..e71d4dac6ac9d --- /dev/null +++ b/packages/use-sync-external-store/shim/with-selector/index.js @@ -0,0 +1,12 @@ +/** + * 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 + */ + +'use strict'; + +export {useSyncExternalStoreWithSelector} from 'use-sync-external-store/src/useSyncExternalStoreWithSelector'; diff --git a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreNative-test.js b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreNative-test.js index 0902e7554c450..caedd74f74470 100644 --- a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreNative-test.js +++ b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreNative-test.js @@ -15,7 +15,7 @@ let React; let ReactNoop; let Scheduler; let useSyncExternalStore; -let useSyncExternalStoreExtra; +let useSyncExternalStoreWithSelector; let act; // This tests the userspace shim of `useSyncExternalStore` in a server-rendering @@ -36,25 +36,40 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => { startTransition: _, // eslint-disable-next-line no-unused-vars useSyncExternalStore: __, - // eslint-disable-next-line no-unused-vars - unstable_useSyncExternalStore: ___, ...otherExports } = jest.requireActual('react'); return otherExports; }); - jest.mock('use-sync-external-store', () => - jest.requireActual('use-sync-external-store/index.native'), + jest.mock('use-sync-external-store/shim', () => + jest.requireActual('use-sync-external-store/shim/index.native'), ); React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); act = require('jest-react').act; - useSyncExternalStore = require('use-sync-external-store') + + if (gate(flags => flags.source)) { + // The `shim/with-selector` module composes the main + // `use-sync-external-store` entrypoint. In the compiled artifacts, this + // is resolved to the `shim` implementation by our build config, but when + // running the tests against the source files, we need to tell Jest how to + // resolve it. Because this is a source module, this mock has no affect on + // the build tests. + jest.mock('use-sync-external-store/src/useSyncExternalStore', () => + jest.requireActual('use-sync-external-store/shim'), + ); + jest.mock('use-sync-external-store/src/isServerEnvironment', () => + jest.requireActual( + 'use-sync-external-store/src/forks/isServerEnvironment.native', + ), + ); + } + useSyncExternalStore = require('use-sync-external-store/shim') .useSyncExternalStore; - useSyncExternalStoreExtra = require('use-sync-external-store/extra') - .useSyncExternalStoreExtra; + useSyncExternalStoreWithSelector = require('use-sync-external-store/shim/with-selector') + .useSyncExternalStoreWithSelector; }); function Text({text}) { @@ -105,32 +120,12 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => { expect(root).toMatchRenderedOutput('client'); }); - test('native version', async () => { - const store = createExternalStore('client'); - - function App() { - const text = useSyncExternalStore( - store.subscribe, - store.getState, - () => 'server', - ); - return ; - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render(); - }); - expect(Scheduler).toHaveYielded(['client']); - expect(root).toMatchRenderedOutput('client'); - }); - // @gate !(enableUseRefAccessWarning && __DEV__) test('Using isEqual to bailout', async () => { const store = createExternalStore({a: 0, b: 0}); function A() { - const {a} = useSyncExternalStoreExtra( + const {a} = useSyncExternalStoreWithSelector( store.subscribe, store.getState, null, @@ -140,7 +135,7 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => { return ; } function B() { - const {b} = useSyncExternalStoreExtra( + const {b} = useSyncExternalStoreWithSelector( store.subscribe, store.getState, null, diff --git a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js index ab8143f24034c..5af9e9767ab0c 100644 --- a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js +++ b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js @@ -10,7 +10,7 @@ 'use strict'; let useSyncExternalStore; -let useSyncExternalStoreExtra; +let useSyncExternalStoreWithSelector; let React; let ReactDOM; let Scheduler; @@ -25,11 +25,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { beforeEach(() => { jest.resetModules(); - // Remove the built-in API from the React exports to force the package to - // use the shim. - if (!gate(flags => flags.supportsNativeUseSyncExternalStore)) { - // and the non-variant tests for the shim. - // + if (gate(flags => flags.enableUseSyncExternalStoreShim)) { // Remove useSyncExternalStore from the React imports so that we use the // shim instead. Also removing startTransition, since we use that to // detect outdated 18 alphas that don't yet include useSyncExternalStore. @@ -42,8 +38,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { startTransition: _, // eslint-disable-next-line no-unused-vars useSyncExternalStore: __, - // eslint-disable-next-line no-unused-vars - unstable_useSyncExternalStore: ___, ...otherExports } = jest.requireActual('react'); return otherExports; @@ -64,10 +58,21 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { // in both concurrent and legacy mode, I'm adding batching here. act = cb => internalAct(() => ReactDOM.unstable_batchedUpdates(cb)); - useSyncExternalStore = require('use-sync-external-store') + if (gate(flags => flags.source)) { + // The `shim/with-selector` module composes the main + // `use-sync-external-store` entrypoint. In the compiled artifacts, this + // is resolved to the `shim` implementation by our build config, but when + // running the tests against the source files, we need to tell Jest how to + // resolve it. Because this is a source module, this mock has no affect on + // the build tests. + jest.mock('use-sync-external-store/src/useSyncExternalStore', () => + jest.requireActual('use-sync-external-store/shim'), + ); + } + useSyncExternalStore = require('use-sync-external-store/shim') .useSyncExternalStore; - useSyncExternalStoreExtra = require('use-sync-external-store/extra') - .useSyncExternalStoreExtra; + useSyncExternalStoreWithSelector = require('use-sync-external-store/shim/with-selector') + .useSyncExternalStoreWithSelector; }); function Text({text}) { @@ -78,7 +83,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { function createRoot(container) { // This wrapper function exists so we can test both legacy roots and // concurrent roots. - if (gate(flags => flags.supportsNativeUseSyncExternalStore)) { + if (gate(flags => !flags.enableUseSyncExternalStoreShim)) { // The native implementation only exists in 18+, so we test using // concurrent mode. To test the legacy root behavior in the native // implementation (which is supported in the sense that it needs to have @@ -265,7 +270,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { // In React 18, you can't observe in between a sync render and its // passive effects, so this is only relevant to legacy roots - // @gate !supportsNativeUseSyncExternalStore + // @gate enableUseSyncExternalStoreShim test( "compares to current state before bailing out, even when there's a " + 'mutation in between the sync and passive effects', @@ -547,7 +552,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { await act(() => { store.set({value: 1, throwInGetSnapshot: true, throwInIsEqual: false}); }); - if (gate(flags => flags.supportsNativeUseSyncExternalStore)) { + if (gate(flags => !flags.enableUseSyncExternalStoreShim)) { expect(Scheduler).toHaveYielded([ 'Error in getSnapshot', // In a concurrent root, React renders a second time to attempt to @@ -595,7 +600,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { function App() { Scheduler.unstable_yieldValue('App'); - const a = useSyncExternalStoreExtra( + const a = useSyncExternalStoreWithSelector( store.subscribe, store.getState, null, @@ -632,7 +637,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { const store = createExternalStore({a: 0, b: 0}); function A() { - const {a} = useSyncExternalStoreExtra( + const {a} = useSyncExternalStoreWithSelector( store.subscribe, store.getState, null, @@ -642,7 +647,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { return ; } function B() { - const {b} = useSyncExternalStoreExtra( + const {b} = useSyncExternalStoreWithSelector( store.subscribe, store.getState, null, @@ -711,7 +716,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { container.innerHTML = '
server
'; const serverRenderedDiv = container.getElementsByTagName('div')[0]; - if (gate(flags => flags.supportsNativeUseSyncExternalStore)) { + if (gate(flags => !flags.enableUseSyncExternalStoreShim)) { act(() => { ReactDOM.hydrateRoot(container, ); }); @@ -774,7 +779,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { Scheduler.unstable_yieldValue('Inline selector'); return [...state.items, 'C']; }; - const items = useSyncExternalStoreExtra( + const items = useSyncExternalStoreWithSelector( store.subscribe, store.getState, null, @@ -842,7 +847,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { const selector = state => state.a.toUpperCase(); function App() { - const a = useSyncExternalStoreExtra( + const a = useSyncExternalStoreWithSelector( store.subscribe, store.getState, null, @@ -877,7 +882,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => { const isEqual = (left, right) => left.a.trim() === right.a.trim(); function App() { - const a = useSyncExternalStoreExtra( + const a = useSyncExternalStoreWithSelector( store.subscribe, store.getState, null, diff --git a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShimServer-test.js b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShimServer-test.js index b46e028724061..016240a4d3f2f 100644 --- a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShimServer-test.js +++ b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShimServer-test.js @@ -35,8 +35,6 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => { startTransition: _, // eslint-disable-next-line no-unused-vars useSyncExternalStore: __, - // eslint-disable-next-line no-unused-vars - unstable_useSyncExternalStore: ___, ...otherExports } = jest.requireActual('react'); return otherExports; @@ -47,7 +45,7 @@ describe('useSyncExternalStore (userspace shim, server rendering)', () => { ReactDOMServer = require('react-dom/server'); Scheduler = require('scheduler'); - useSyncExternalStore = require('use-sync-external-store') + useSyncExternalStore = require('use-sync-external-store/shim') .useSyncExternalStore; }); diff --git a/packages/use-sync-external-store/extra.js b/packages/use-sync-external-store/src/forks/isServerEnvironment.native.js similarity index 75% rename from packages/use-sync-external-store/extra.js rename to packages/use-sync-external-store/src/forks/isServerEnvironment.native.js index 90d48eb4641b6..2bd359085541d 100644 --- a/packages/use-sync-external-store/extra.js +++ b/packages/use-sync-external-store/src/forks/isServerEnvironment.native.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -export * from './src/useSyncExternalStoreExtra'; +export const isServerEnvironment = false; diff --git a/packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-built-in.js b/packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-built-in.js new file mode 100644 index 0000000000000..049f0c887e278 --- /dev/null +++ b/packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-built-in.js @@ -0,0 +1,16 @@ +/** + * 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 + */ + +'use strict'; + +// Intentionally not using named imports because Rollup uses dynamic +// dispatch for CommonJS interop named imports. +import * as React from 'react'; + +export const useSyncExternalStore = React.useSyncExternalStore; diff --git a/packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-shim.js b/packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-shim.js new file mode 100644 index 0000000000000..54425c3ae0e2d --- /dev/null +++ b/packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-shim.js @@ -0,0 +1,16 @@ +/** + * 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 + */ + +'use strict'; + +// Intentionally not using named imports because Rollup uses dynamic +// dispatch for CommonJS interop named imports. +import * as shim from 'use-sync-external-store/shim'; + +export const useSyncExternalStore = shim.useSyncExternalStore; diff --git a/packages/use-sync-external-store/src/isServerEnvironment.js b/packages/use-sync-external-store/src/isServerEnvironment.js new file mode 100644 index 0000000000000..30adcb934731f --- /dev/null +++ b/packages/use-sync-external-store/src/isServerEnvironment.js @@ -0,0 +1,12 @@ +/** + * 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 {canUseDOM} from 'shared/ExecutionEnvironment'; + +export const isServerEnvironment = !canUseDOM; diff --git a/packages/use-sync-external-store/src/useSyncExternalStore.js b/packages/use-sync-external-store/src/useSyncExternalStore.js index c152287174269..f1bb95556d2bb 100644 --- a/packages/use-sync-external-store/src/useSyncExternalStore.js +++ b/packages/use-sync-external-store/src/useSyncExternalStore.js @@ -7,16 +7,24 @@ * @flow */ -import {canUseDOM} from 'shared/ExecutionEnvironment'; -import {useSyncExternalStore as client} from './useSyncExternalStoreClient'; -import {useSyncExternalStore as server} from './useSyncExternalStoreServer'; +'use strict'; + +// Intentionally not using named imports because Rollup uses dynamic +// dispatch for CommonJS interop named imports. import * as React from 'react'; -const {unstable_useSyncExternalStore: builtInAPI} = React; +export const useSyncExternalStore = React.useSyncExternalStore; -export const useSyncExternalStore = - builtInAPI !== undefined - ? ((builtInAPI: any): typeof client) - : canUseDOM - ? client - : server; +if (__DEV__) { + console.error( + "The main 'use-sync-external-store' entry point is not supported; all it " + + "does is re-export useSyncExternalStore from the 'react' package, so " + + 'it only works with React 18+.' + + '\n\n' + + 'If you wish to support React 16 and 17, import from ' + + "'use-sync-external-store/shim' instead. It will fall back to a shimmed" + + 'implementation when the native one is not available.' + + '\n\n' + + "If you only support React 18+, you can import directly from 'react'.", + ); +} diff --git a/packages/use-sync-external-store/src/useSyncExternalStoreShim.js b/packages/use-sync-external-store/src/useSyncExternalStoreShim.js new file mode 100644 index 0000000000000..82d944de1dd1d --- /dev/null +++ b/packages/use-sync-external-store/src/useSyncExternalStoreShim.js @@ -0,0 +1,18 @@ +/** + * 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 {useSyncExternalStore as client} from './useSyncExternalStoreShimClient'; +import {useSyncExternalStore as server} from './useSyncExternalStoreShimServer'; +import {isServerEnvironment} from './isServerEnvironment'; +import {useSyncExternalStore as builtInAPI} from 'react'; + +const shim = isServerEnvironment ? server : client; + +export const useSyncExternalStore = + builtInAPI !== undefined ? ((builtInAPI: any): typeof shim) : shim; diff --git a/packages/use-sync-external-store/src/useSyncExternalStoreClient.js b/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js similarity index 95% rename from packages/use-sync-external-store/src/useSyncExternalStoreClient.js rename to packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js index dc42169c399d6..8578b3d3fd1c7 100644 --- a/packages/use-sync-external-store/src/useSyncExternalStoreClient.js +++ b/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js @@ -30,10 +30,10 @@ let didWarnUncachedGetSnapshot = false; export function useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, - // Note: The client shim does not use getServerSnapshot, because pre-18 - // versions of React do not expose a way to check if we're hydrating. So - // users of the shim will need to track that themselves and return the - // correct value from `getSnapshot`. + // Note: The shim does not use getServerSnapshot, because pre-18 versions of + // React do not expose a way to check if we're hydrating. So users of the shim + // will need to track that themselves and return the correct value + // from `getSnapshot`. getServerSnapshot?: () => T, ): T { if (__DEV__) { diff --git a/packages/use-sync-external-store/src/useSyncExternalStoreServer.js b/packages/use-sync-external-store/src/useSyncExternalStoreShimServer.js similarity index 59% rename from packages/use-sync-external-store/src/useSyncExternalStoreServer.js rename to packages/use-sync-external-store/src/useSyncExternalStoreShimServer.js index 52903dd4aca89..4f2432718c365 100644 --- a/packages/use-sync-external-store/src/useSyncExternalStoreServer.js +++ b/packages/use-sync-external-store/src/useSyncExternalStoreShimServer.js @@ -12,5 +12,9 @@ export function useSyncExternalStore( getSnapshot: () => T, getServerSnapshot?: () => T, ): T { + // Note: The shim does not use getServerSnapshot, because pre-18 versions of + // React do not expose a way to check if we're hydrating. So users of the shim + // will need to track that themselves and return the correct value + // from `getSnapshot`. return getSnapshot(); } diff --git a/packages/use-sync-external-store/src/useSyncExternalStoreExtra.js b/packages/use-sync-external-store/src/useSyncExternalStoreWithSelector.js similarity index 95% rename from packages/use-sync-external-store/src/useSyncExternalStoreExtra.js rename to packages/use-sync-external-store/src/useSyncExternalStoreWithSelector.js index aa4957b534753..c7012e615ecd1 100644 --- a/packages/use-sync-external-store/src/useSyncExternalStoreExtra.js +++ b/packages/use-sync-external-store/src/useSyncExternalStoreWithSelector.js @@ -9,14 +9,14 @@ import * as React from 'react'; import is from 'shared/objectIs'; -import {useSyncExternalStore} from 'use-sync-external-store'; +import {useSyncExternalStore} from 'use-sync-external-store/src/useSyncExternalStore'; -// Intentionally not using named imports because Rollup uses dynamic -// dispatch for CommonJS interop named imports. +// Intentionally not using named imports because Rollup uses dynamic dispatch +// for CommonJS interop. const {useRef, useEffect, useMemo, useDebugValue} = React; // Same as useSyncExternalStore, but supports selector and isEqual arguments. -export function useSyncExternalStoreExtra( +export function useSyncExternalStoreWithSelector( subscribe: (() => void) => () => void, getSnapshot: () => Snapshot, getServerSnapshot: void | null | (() => Snapshot), diff --git a/packages/use-sync-external-store/with-selector.js b/packages/use-sync-external-store/with-selector.js new file mode 100644 index 0000000000000..e71d4dac6ac9d --- /dev/null +++ b/packages/use-sync-external-store/with-selector.js @@ -0,0 +1,12 @@ +/** + * 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 + */ + +'use strict'; + +export {useSyncExternalStoreWithSelector} from 'use-sync-external-store/src/useSyncExternalStoreWithSelector'; diff --git a/scripts/jest/TestFlags.js b/scripts/jest/TestFlags.js index c1bd03e00b1a9..af83baf0f3ec9 100644 --- a/scripts/jest/TestFlags.js +++ b/scripts/jest/TestFlags.js @@ -84,9 +84,8 @@ function getTestFlags() { source: !process.env.IS_BUILD, www, - // This isn't a flag, just a useful alias for tests. Remove once - // useSyncExternalStore lands in the `next` channel. - supportsNativeUseSyncExternalStore: __EXPERIMENTAL__ || www, + // This isn't a flag, just a useful alias for tests. + enableUseSyncExternalStoreShim: !__VARIANT__, // If there's a naming conflict between scheduler and React feature flags, the // React ones take precedence. diff --git a/scripts/jest/config.build.js b/scripts/jest/config.build.js index 69cc7d18c536c..5b04ab05df7cd 100644 --- a/scripts/jest/config.build.js +++ b/scripts/jest/config.build.js @@ -45,6 +45,13 @@ packages.forEach(name => { ] = `/build/${NODE_MODULES_DIR}/${name}/$1`; }); +moduleNameMapper[ + 'use-sync-external-store/shim/with-selector' +] = `/build/${NODE_MODULES_DIR}/use-sync-external-store/shim/with-selector`; +moduleNameMapper[ + 'use-sync-external-store/shim/index.native' +] = `/build/${NODE_MODULES_DIR}/use-sync-external-store/shim/index.native`; + module.exports = Object.assign({}, baseConfig, { // Redirect imports to the compiled bundles moduleNameMapper, diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 40ae43ae7cbc0..8b12e8550e365 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -789,7 +789,7 @@ const bundles = [ externals: ['react'], }, - /******* Shim for useSyncExternalStore *******/ + /******* useSyncExternalStore *******/ { bundleTypes: [NODE_DEV, NODE_PROD], moduleType: ISOMORPHIC, @@ -800,26 +800,48 @@ const bundles = [ externals: ['react'], }, - /******* Shim for useSyncExternalStore (+ extra user-space features) *******/ + /******* useSyncExternalStore (shim) *******/ { bundleTypes: [NODE_DEV, NODE_PROD], moduleType: ISOMORPHIC, - entry: 'use-sync-external-store/extra', - global: 'useSyncExternalStoreExtra', - minifyWithProdErrorCodes: true, + entry: 'use-sync-external-store/shim', + global: 'useSyncExternalStore', + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: true, - externals: ['react', 'use-sync-external-store'], + externals: ['react'], }, - /******* Shim for useSyncExternalStore ReactNative *******/ + /******* useSyncExternalStore (shim, native) *******/ { bundleTypes: [NODE_DEV, NODE_PROD], moduleType: ISOMORPHIC, - entry: 'use-sync-external-store/index.native', - global: 'useSyncExternalStoreNative', - minifyWithProdErrorCodes: true, + entry: 'use-sync-external-store/shim/index.native', + global: 'useSyncExternalStore', + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: true, - externals: ['react', 'ReactNativeInternalFeatureFlags'], + externals: ['react'], + }, + + /******* useSyncExternalStoreWithSelector *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: ISOMORPHIC, + entry: 'use-sync-external-store/with-selector', + global: 'useSyncExternalStoreWithSelector', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: true, + externals: ['react'], + }, + + /******* useSyncExternalStoreWithSelector (shim) *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: ISOMORPHIC, + entry: 'use-sync-external-store/shim/with-selector', + global: 'useSyncExternalStoreWithSelector', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: true, + externals: ['react', 'use-sync-external-store/shim'], }, /******* React Scheduler (experimental) *******/ diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index 84211da5c5125..b2234fa42a010 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -481,6 +481,24 @@ const forks = Object.freeze({ return null; } }, + + 'use-sync-external-store/src/useSyncExternalStore': (bundleType, entry) => { + if (entry.startsWith('use-sync-external-store/shim')) { + return 'use-sync-external-store/src/forks/useSyncExternalStore.forward-to-shim'; + } + if (entry !== 'use-sync-external-store') { + // Internal modules that aren't shims should use the native API from the + // react package. + return 'use-sync-external-store/src/forks/useSyncExternalStore.forward-to-built-in'; + } + return null; + }, + + 'use-sync-external-store/src/isServerEnvironment': (bundleType, entry) => { + if (entry.endsWith('.native')) { + return 'use-sync-external-store/src/forks/isServerEnvironment.native'; + } + }, }); module.exports = forks; diff --git a/scripts/shared/pathsByLanguageVersion.js b/scripts/shared/pathsByLanguageVersion.js index af6963f78949e..8c60a9c074a5a 100644 --- a/scripts/shared/pathsByLanguageVersion.js +++ b/scripts/shared/pathsByLanguageVersion.js @@ -11,6 +11,8 @@ const esNextPaths = [ // Internal forwarding modules 'packages/*/*.js', 'packages/*/esm/*.js', + 'packages/use-sync-external-store/shim/**/*.js', + 'packages/use-sync-external-store/with-selector/**/*.js', // Source files 'packages/*/src/**/*.js', 'packages/dom-event-testing-library/**/*.js', From 6c7ef3fce5547da5db3d400ccf95a5023f8891f4 Mon Sep 17 00:00:00 2001 From: Juan Date: Sun, 31 Oct 2021 19:19:02 -0400 Subject: [PATCH 070/109] React DevTools 4.20.2 -> 4.21.0 (#22661) --- packages/react-devtools-core/package.json | 2 +- .../chrome/manifest.json | 4 ++-- .../edge/manifest.json | 4 ++-- .../firefox/manifest.json | 2 +- packages/react-devtools-inline/package.json | 2 +- .../package.json | 2 +- packages/react-devtools/CHANGELOG.md | 17 +++++++++++++++++ packages/react-devtools/package.json | 4 ++-- 8 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json index d57fc10ff97ec..9017ff630310a 100644 --- a/packages/react-devtools-core/package.json +++ b/packages/react-devtools-core/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-core", - "version": "4.20.2", + "version": "4.21.0", "description": "Use react-devtools outside of the browser", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index ecafb54ca6d34..780793b6d1e02 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Chrome Developer Tools.", - "version": "4.20.2", - "version_name": "4.20.2", + "version": "4.21.0", + "version_name": "4.21.0", "minimum_chrome_version": "60", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index 6b813661e48d6..fa9b34cd2af61 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.", - "version": "4.20.2", - "version_name": "4.20.2", + "version": "4.21.0", + "version_name": "4.21.0", "minimum_chrome_version": "60", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 16f4e7821e476..2bd83fd65679e 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", - "version": "4.20.2", + "version": "4.21.0", "applications": { "gecko": { "id": "@react-devtools", diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index 7e013af63677b..b814eacd4d193 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-inline", - "version": "4.20.2", + "version": "4.21.0", "description": "Embed react-devtools within a website", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-scheduling-profiler/package.json b/packages/react-devtools-scheduling-profiler/package.json index 84fdf9d470fa2..a74b66fa1d55f 100644 --- a/packages/react-devtools-scheduling-profiler/package.json +++ b/packages/react-devtools-scheduling-profiler/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "react-devtools-scheduling-profiler", - "version": "4.20.2", + "version": "4.21.0", "license": "MIT", "dependencies": { "@elg/speedscope": "1.9.0-a6f84db", diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index 8f916003d79ff..95a1a65721a7c 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -2,6 +2,23 @@ +## 4.21.0 (October 31, 2021) + +#### Features +* Scheduling Profiler: Add marks for component effects (mount and unmount) ([@bvaughn](https://github.com/bvaughn) in [#22578](https://github.com/facebook/react/pull/22578)) +* Scheduling Profiler: De-emphasize React internal frames ([bvaughn](https://github.com/bvaughn) in [#22588](https://github.com/facebook/react/pull/22588)) + + +#### Bugfix +* Revert logic for checking for duplicate installations of DevTools potentially causing issues loading Components tab ([@jstejada](https://github.com/jstejada) in [#22638](https://github.com/facebook/react/pull/22638)) +* Scheduling Profiler does not warn about long transitions ([@bvaughn](https://github.com/bvaughn) in [#22614](https://github.com/facebook/react/pull/22614)) +* Re-enable 'Reload and Start Profiling' for Microsoft Edge ([@eoandersson](https://github.com/eoandersson) in [#22631](https://github.com/facebook/react/pull/22631)) + + +#### Misc +* DevTools supports ENV-injected version for better internal bug reports ([@bvaughn](https://github.com/bvaughn) in [#22635](https://github.com/facebook/react/pull/22635)) +* Fix typos ([@KonstHardy](https://github.com/KonstHardy) in [#22494](https://github.com/facebook/react/pull/22494)) + ## 4.20.2 (October 20, 2021) #### Bugfix diff --git a/packages/react-devtools/package.json b/packages/react-devtools/package.json index 00240aa38200b..ca66824859ca4 100644 --- a/packages/react-devtools/package.json +++ b/packages/react-devtools/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools", - "version": "4.20.2", + "version": "4.21.0", "description": "Use react-devtools outside of the browser", "license": "MIT", "repository": { @@ -27,7 +27,7 @@ "electron": "^11.1.0", "ip": "^1.1.4", "minimist": "^1.2.3", - "react-devtools-core": "4.20.2", + "react-devtools-core": "4.21.0", "update-notifier": "^2.1.0" } } From 9db8713f9d08c1956939e33cc2da25c867748263 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 31 Oct 2021 21:01:39 -0400 Subject: [PATCH 071/109] Pin CI to Node 14 (#22665) CI starting running Node 16, which breaks some of our tests because the error message text for undefined property access has changed. We should pin to Node 14 until we are able to update the messages. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8c847fc05f39e..843e2f5fba4a4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2.1 aliases: - &docker - - image: circleci/openjdk:8-jdk-node-browsers + - image: cimg/openjdk:17.0.0-node - &environment TZ: /usr/share/zoneinfo/America/Los_Angeles From a0d991fe6587ad1cd1a97230f62f82c7cb6b9a40 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 1 Nov 2021 00:39:51 -0400 Subject: [PATCH 072/109] Re-land #22292 (remove uMS from open source build) (#22664) I had to revert #22292 because there are some internal callers of useMutableSource that we haven't migrated yet. This removes useMutableSource from the open source build but keeps it in the internal one. --- .../ReactHooksInspectionIntegration-test.js | 1 + .../src/ReactFiberHooks.new.js | 13 ++++++++ .../src/ReactFiberHooks.old.js | 13 ++++++++ .../useMutableSource-test.internal.js | 31 +++++++++++++++++++ .../useMutableSourceHydration-test.js | 5 +++ packages/react/index.experimental.js | 1 - packages/react/index.stable.js | 1 - packages/shared/ReactFeatureFlags.js | 3 ++ .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.native.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 3 ++ .../shared/forks/ReactFeatureFlags.testing.js | 1 + .../forks/ReactFeatureFlags.testing.www.js | 3 ++ .../shared/forks/ReactFeatureFlags.www.js | 3 ++ 16 files changed, 80 insertions(+), 2 deletions(-) diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 087d74d628724..3a6ac01f98161 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -1018,6 +1018,7 @@ describe('ReactHooksInspectionIntegration', () => { ]); }); + // @gate enableUseMutableSource it('should support composite useMutableSource hook', () => { const createMutableSource = React.createMutableSource || React.unstable_createMutableSource; diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index c3b76549a22aa..732f7d71a7e71 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -31,6 +31,7 @@ import { enableStrictEffects, enableLazyContextPropagation, enableSuspenseLayoutEffectSemantics, + enableUseMutableSource, } from 'shared/ReactFeatureFlags'; import { @@ -1052,6 +1053,10 @@ function useMutableSource( getSnapshot: MutableSourceGetSnapshotFn, subscribe: MutableSourceSubscribeFn, ): Snapshot { + if (!enableUseMutableSource) { + return (undefined: any); + } + const root = ((getWorkInProgressRoot(): any): FiberRoot); if (root === null) { @@ -1213,6 +1218,10 @@ function mountMutableSource( getSnapshot: MutableSourceGetSnapshotFn, subscribe: MutableSourceSubscribeFn, ): Snapshot { + if (!enableUseMutableSource) { + return (undefined: any); + } + const hook = mountWorkInProgressHook(); hook.memoizedState = ({ refs: { @@ -1230,6 +1239,10 @@ function updateMutableSource( getSnapshot: MutableSourceGetSnapshotFn, subscribe: MutableSourceSubscribeFn, ): Snapshot { + if (!enableUseMutableSource) { + return (undefined: any); + } + const hook = updateWorkInProgressHook(); return useMutableSource(hook, source, getSnapshot, subscribe); } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 8bc1510deb455..b78f24e8b47f8 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -31,6 +31,7 @@ import { enableStrictEffects, enableLazyContextPropagation, enableSuspenseLayoutEffectSemantics, + enableUseMutableSource, } from 'shared/ReactFeatureFlags'; import { @@ -1052,6 +1053,10 @@ function useMutableSource( getSnapshot: MutableSourceGetSnapshotFn, subscribe: MutableSourceSubscribeFn, ): Snapshot { + if (!enableUseMutableSource) { + return (undefined: any); + } + const root = ((getWorkInProgressRoot(): any): FiberRoot); if (root === null) { @@ -1213,6 +1218,10 @@ function mountMutableSource( getSnapshot: MutableSourceGetSnapshotFn, subscribe: MutableSourceSubscribeFn, ): Snapshot { + if (!enableUseMutableSource) { + return (undefined: any); + } + const hook = mountWorkInProgressHook(); hook.memoizedState = ({ refs: { @@ -1230,6 +1239,10 @@ function updateMutableSource( getSnapshot: MutableSourceGetSnapshotFn, subscribe: MutableSourceSubscribeFn, ): Snapshot { + if (!enableUseMutableSource) { + return (undefined: any); + } + const hook = updateWorkInProgressHook(); return useMutableSource(hook, source, getSnapshot, subscribe); } diff --git a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js index a6d99f9802084..11cabd6f3175a 100644 --- a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js +++ b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js @@ -141,6 +141,7 @@ describe('useMutableSource', () => { beforeEach(loadModules); + // @gate enableUseMutableSource it('should subscribe to a source and schedule updates when it changes', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -208,6 +209,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should restart work if a new source is mutated during render', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -263,6 +265,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should schedule an update if a new source is mutated between render and commit (subscription)', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -302,6 +305,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should unsubscribe and resubscribe if a new source is used', () => { const sourceA = createSource('a-one'); const mutableSourceA = createMutableSource( @@ -358,6 +362,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should unsubscribe and resubscribe if a new subscribe function is provided', () => { const source = createSource('a-one'); const mutableSource = createMutableSource(source, param => param.version); @@ -422,6 +427,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should re-use previously read snapshot value when reading is unsafe', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -484,6 +490,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should read from source on newly mounted subtree if no pending updates are scheduled for source', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -523,6 +530,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should throw and restart render if source and snapshot are unavailable during an update', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -586,6 +594,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should throw and restart render if source and snapshot are unavailable during a sync update', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -649,6 +658,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should only update components whose subscriptions fire', () => { const source = createComplexSource('a:one', 'b:one'); const mutableSource = createMutableSource(source, param => param.version); @@ -687,6 +697,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should detect tearing in part of the store not yet subscribed to', () => { const source = createComplexSource('a:one', 'b:one'); const mutableSource = createMutableSource(source, param => param.version); @@ -779,6 +790,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('does not schedule an update for subscriptions that fire with an unchanged snapshot', () => { const MockComponent = jest.fn(Component); @@ -805,6 +817,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should throw and restart if getSnapshot changes between scheduled update and re-render', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -845,6 +858,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should recover from a mutation during yield when other work is scheduled', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -899,6 +913,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should not throw if the new getSnapshot returns the same snapshot value', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -953,6 +968,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should not throw if getSnapshot changes but the source can be safely read from anyway', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -992,6 +1008,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should still schedule an update if an eager selector throws after a mutation', () => { const source = createSource({ friends: [ @@ -1058,6 +1075,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should not warn about updates that fire between unmount and passive unsubscribe', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -1094,6 +1112,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should support inline selectors and updates that are processed after selector change', async () => { const source = createSource({ a: 'initial', @@ -1138,6 +1157,7 @@ describe('useMutableSource', () => { expect(root).toMatchRenderedOutput('Another update'); }); + // @gate enableUseMutableSource it('should clear the update queue when getSnapshot changes with pending lower priority updates', async () => { const source = createSource({ a: 'initial', @@ -1194,6 +1214,7 @@ describe('useMutableSource', () => { expect(root).toMatchRenderedOutput('B: Update'); }); + // @gate enableUseMutableSource it('should clear the update queue when source changes with pending lower priority updates', async () => { const sourceA = createSource('initial'); const sourceB = createSource('initial'); @@ -1238,6 +1259,7 @@ describe('useMutableSource', () => { expect(root).toMatchRenderedOutput('B: Update'); }); + // @gate enableUseMutableSource it('should always treat reading as potentially unsafe when getSnapshot changes between renders', async () => { const source = createSource({ a: 'foo', @@ -1327,6 +1349,7 @@ describe('useMutableSource', () => { expect(Scheduler).toHaveYielded(['x: bar, y: bar']); }); + // @gate enableUseMutableSource it('getSnapshot changes and then source is mutated in between paint and passive effect phase', async () => { const source = createSource({ a: 'foo', @@ -1385,6 +1408,7 @@ describe('useMutableSource', () => { expect(root).toMatchRenderedOutput('baz'); }); + // @gate enableUseMutableSource it('getSnapshot changes and then source is mutated in between paint and passive effect phase, case 2', async () => { const source = createSource({ a: 'a0', @@ -1455,6 +1479,7 @@ describe('useMutableSource', () => { expect(root.getChildrenAsJSX()).toEqual('first: a1, second: a1'); }); + // @gate enableUseMutableSource it( 'if source is mutated after initial read but before subscription is set ' + 'up, should still entangle all pending mutations even if snapshot of ' + @@ -1559,6 +1584,7 @@ describe('useMutableSource', () => { }, ); + // @gate enableUseMutableSource it('warns about functions being used as snapshot values', async () => { const source = createSource(() => 'a'); const mutableSource = createMutableSource(source, param => param.version); @@ -1586,6 +1612,7 @@ describe('useMutableSource', () => { expect(root).toMatchRenderedOutput('a'); }); + // @gate enableUseMutableSource it('getSnapshot changes and then source is mutated during interleaved event', async () => { const {useEffect} = React; @@ -1710,6 +1737,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should not tear with newly mounted component when updates were scheduled at a lower priority', async () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -1789,6 +1817,7 @@ describe('useMutableSource', () => { if (__DEV__) { describe('dev warnings', () => { + // @gate enableUseMutableSource it('should warn if the subscribe function does not return an unsubscribe function', () => { const source = createSource('one'); const mutableSource = createMutableSource( @@ -1814,6 +1843,7 @@ describe('useMutableSource', () => { ); }); + // @gate enableUseMutableSource it('should error if multiple renderers of the same type use a mutable source at the same time', () => { const source = createSource('one'); const mutableSource = createMutableSource( @@ -1894,6 +1924,7 @@ describe('useMutableSource', () => { }); }); + // @gate enableUseMutableSource it('should error if multiple renderers of the same type use a mutable source at the same time with mutation between', () => { const source = createSource('one'); const mutableSource = createMutableSource( diff --git a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js index 7f46d1cb00552..61ebbe45e90c1 100644 --- a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js +++ b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js @@ -144,6 +144,7 @@ describe('useMutableSourceHydration', () => { return
{`${label}:${snapshot}`}
; } + // @gate enableUseMutableSource it('should render and hydrate', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -180,6 +181,7 @@ describe('useMutableSourceHydration', () => { expect(source.listenerCount).toBe(1); }); + // @gate enableUseMutableSource it('should detect a tear before hydrating a component', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -224,6 +226,7 @@ describe('useMutableSourceHydration', () => { expect(source.listenerCount).toBe(1); }); + // @gate enableUseMutableSource it('should detect a tear between hydrating components', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); @@ -282,6 +285,7 @@ describe('useMutableSourceHydration', () => { expect(source.listenerCount).toBe(2); }); + // @gate enableUseMutableSource it('should detect a tear between hydrating components reading from different parts of a source', () => { const source = createComplexSource('a:one', 'b:one'); const mutableSource = createMutableSource(source, param => param.version); @@ -371,6 +375,7 @@ describe('useMutableSourceHydration', () => { }); // @gate !enableSyncDefaultUpdates + // @gate enableUseMutableSource it('should detect a tear during a higher priority interruption', () => { const source = createSource('one'); const mutableSource = createMutableSource(source, param => param.version); diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index 4b4fa89e01898..24fc9782595d4 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -46,7 +46,6 @@ export { useInsertionEffect, useLayoutEffect, useMemo, - useMutableSource as unstable_useMutableSource, useSyncExternalStore, useReducer, useRef, diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index 3a0600d11a713..867980fa5389d 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -39,7 +39,6 @@ export { useInsertionEffect, useLayoutEffect, useMemo, - useMutableSource as unstable_useMutableSource, useSyncExternalStore, useReducer, useRef, diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index f5d34e2ff6539..112c2d10f2cbd 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -185,3 +185,6 @@ export const allowConcurrentByDefault = false; export const enablePersistentOffscreenHostContainer = false; export const consoleManagedByDevToolsDuringStrictMode = true; + +// Only enabled in www builds +export const enableUseMutableSource = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 15a8c29f71799..b61bbcf15747c 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -74,6 +74,7 @@ export const enableSyncDefaultUpdates = true; export const allowConcurrentByDefault = true; export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableUseMutableSource = false; // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 3c11070d6ecb3..7e24f2a0a2710 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -66,6 +66,7 @@ export const allowConcurrentByDefault = false; export const enablePersistentOffscreenHostContainer = false; export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableUseMutableSource = false; // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 4b0457c219587..bce24128a7911 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -66,6 +66,7 @@ export const allowConcurrentByDefault = false; export const enablePersistentOffscreenHostContainer = false; export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableUseMutableSource = false; // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 2c54c1fb77c20..02f063e58a822 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -65,6 +65,7 @@ export const allowConcurrentByDefault = true; export const enablePersistentOffscreenHostContainer = false; export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableUseMutableSource = false; // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 043ac7da254b9..7227c254dccbe 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -67,6 +67,9 @@ export const enablePersistentOffscreenHostContainer = false; export const consoleManagedByDevToolsDuringStrictMode = false; +// Some www surfaces are still using this. Remove once they have been migrated. +export const enableUseMutableSource = true; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index 76af047ab6d4e..264127fc197ef 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -66,6 +66,7 @@ export const allowConcurrentByDefault = false; export const enablePersistentOffscreenHostContainer = false; export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableUseMutableSource = false; // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 9eefdc2cca773..87d7247d6eebe 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -67,6 +67,9 @@ export const enablePersistentOffscreenHostContainer = false; export const consoleManagedByDevToolsDuringStrictMode = false; +// Some www surfaces are still using this. Remove once they have been migrated. +export const enableUseMutableSource = true; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 0f1ce6eb1f9f7..e280e1ff9a6d0 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -102,6 +102,9 @@ export const enablePersistentOffscreenHostContainer = false; export const consoleManagedByDevToolsDuringStrictMode = true; +// Some www surfaces are still using this. Remove once they have been migrated. +export const enableUseMutableSource = true; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; From ebf9ae8579230e7b1ed0b1d243e1cf802f56938b Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 1 Nov 2021 16:30:44 -0400 Subject: [PATCH 073/109] useId (#22644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add useId to dispatcher * Initial useId implementation Ids are base 32 strings whose binary representation corresponds to the position of a node in a tree. Every time the tree forks into multiple children, we add additional bits to the left of the sequence that represent the position of the child within the current level of children. 00101 00010001011010101 ╰─┬─╯ ╰───────┬───────╯ Fork 5 of 20 Parent id The leading 0s are important. In the above example, you only need 3 bits to represent slot 5. However, you need 5 bits to represent all the forks at the current level, so we must account for the empty bits at the end. For this same reason, slots are 1-indexed instead of 0-indexed. Otherwise, the zeroth id at a level would be indistinguishable from its parent. If a node has only one child, and does not materialize an id (i.e. does not contain a useId hook), then we don't need to allocate any space in the sequence. It's treated as a transparent indirection. For example, these two trees produce the same ids: <> <>
However, we cannot skip any materializes an id. Otherwise, a parent id that does not fork would be indistinguishable from its child id. For example, this tree does not fork, but the parent and child must have different ids. To handle this scenario, every time we materialize an id, we allocate a new level with a single slot. You can think of this as a fork with only one prong, or an array of children with length 1. It's possible for the the size of the sequence to exceed 32 bits, the max size for bitwise operations. When this happens, we make more room by converting the right part of the id to a string and storing it in an overflow variable. We use a base 32 string representation, because 32 is the largest power of 2 that is supported by toString(). We want the base to be large so that the resulting ids are compact, and we want the base to be a power of 2 because every log2(base) bits corresponds to a single character, i.e. every log2(32) = 5 bits. That means we can lop bits off the end 5 at a time without affecting the final result. * Incremental hydration Stores the tree context on the dehydrated Suspense boundary's state object so it resume where it left off. * Add useId to react-debug-tools * Add selective hydration test Demonstrates that selective hydration works and ids are preserved even after subsequent client updates. --- .../react-debug-tools/src/ReactDebugHooks.js | 12 + .../ReactHooksInspectionIntegration-test.js | 29 +- .../src/__tests__/ReactDOMUseId-test.js | 515 ++++++++++++++++++ .../src/server/ReactPartialRendererHooks.js | 5 + .../src/ReactChildFiber.new.js | 32 +- .../src/ReactChildFiber.old.js | 32 +- .../src/ReactFiberBeginWork.new.js | 24 + .../src/ReactFiberBeginWork.old.js | 24 + .../src/ReactFiberCompleteWork.new.js | 7 +- .../src/ReactFiberCompleteWork.old.js | 7 +- .../react-reconciler/src/ReactFiberFlags.js | 55 +- .../src/ReactFiberHooks.new.js | 99 ++++ .../src/ReactFiberHooks.old.js | 99 ++++ .../src/ReactFiberHydrationContext.new.js | 10 + .../src/ReactFiberHydrationContext.old.js | 10 + .../src/ReactFiberLane.new.js | 15 +- .../src/ReactFiberLane.old.js | 15 +- .../src/ReactFiberSuspenseComponent.new.js | 3 + .../src/ReactFiberSuspenseComponent.old.js | 3 + .../src/ReactFiberTreeContext.new.js | 273 ++++++++++ .../src/ReactFiberTreeContext.old.js | 273 ++++++++++ .../src/ReactFiberUnwindWork.new.js | 11 + .../src/ReactFiberUnwindWork.old.js | 11 + .../src/ReactInternalTypes.js | 2 + packages/react-reconciler/src/clz32.js | 25 + packages/react-server/src/ReactFizzHooks.js | 39 +- packages/react-server/src/ReactFizzServer.js | 83 ++- .../react-server/src/ReactFizzTreeContext.js | 168 ++++++ .../react-server/src/ReactFlightServer.js | 1 + .../src/ReactSuspenseTestUtils.js | 1 + packages/react/index.classic.fb.js | 1 + packages/react/index.experimental.js | 1 + packages/react/index.js | 1 + packages/react/index.modern.fb.js | 1 + packages/react/index.stable.js | 1 + packages/react/src/React.js | 2 + packages/react/src/ReactHooks.js | 5 + .../unstable-shared-subset.experimental.js | 1 + 38 files changed, 1819 insertions(+), 77 deletions(-) create mode 100644 packages/react-dom/src/__tests__/ReactDOMUseId-test.js create mode 100644 packages/react-reconciler/src/ReactFiberTreeContext.new.js create mode 100644 packages/react-reconciler/src/ReactFiberTreeContext.old.js create mode 100644 packages/react-reconciler/src/clz32.js create mode 100644 packages/react-server/src/ReactFizzTreeContext.js diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 957838ed58a9c..eed8c46df7d6d 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -341,6 +341,17 @@ function useOpaqueIdentifier(): OpaqueIDType | void { 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, @@ -361,6 +372,7 @@ const Dispatcher: DispatcherType = { useSyncExternalStore, useDeferredValue, useOpaqueIdentifier, + useId, }; // Inspect diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 3a6ac01f98161..d17a01a258277 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -628,7 +628,7 @@ describe('ReactHooksInspectionIntegration', () => { it('should support composite useOpaqueIdentifier hook in concurrent mode', () => { function Foo(props) { const id = React.unstable_useOpaqueIdentifier(); - const [state] = React.useState(() => 'hello', []); + const [state] = React.useState('hello'); return
{state}
; } @@ -656,6 +656,33 @@ describe('ReactHooksInspectionIntegration', () => { }); }); + it('should support useId hook', () => { + function Foo(props) { + const id = React.unstable_useId(); + const [state] = React.useState('hello'); + return
{state}
; + } + + const renderer = ReactTestRenderer.create(); + const childFiber = renderer.root.findByType(Foo)._currentFiber(); + const tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + + expect(tree.length).toEqual(2); + + expect(tree[0].id).toEqual(0); + expect(tree[0].isStateEditable).toEqual(false); + expect(tree[0].name).toEqual('Id'); + expect(String(tree[0].value).startsWith('r:')).toBe(true); + + expect(tree[1]).toEqual({ + id: 1, + isStateEditable: true, + name: 'State', + value: 'hello', + subHooks: [], + }); + }); + describe('useDebugValue', () => { it('should support inspectable values for multiple custom hooks', () => { function useLabeledValue(label) { diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js new file mode 100644 index 0000000000000..b61e79fa670d6 --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -0,0 +1,515 @@ +/** + * 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. + * + * @emails react-core + */ + +let JSDOM; +let React; +let ReactDOM; +let Scheduler; +let clientAct; +let ReactDOMFizzServer; +let Stream; +let Suspense; +let useId; +let document; +let writable; +let container; +let buffer = ''; +let hasErrored = false; +let fatalError = undefined; + +describe('useId', () => { + beforeEach(() => { + jest.resetModules(); + JSDOM = require('jsdom').JSDOM; + React = require('react'); + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); + clientAct = require('jest-react').act; + ReactDOMFizzServer = require('react-dom/server'); + Stream = require('stream'); + Suspense = React.Suspense; + useId = React.unstable_useId; + + // Test Environment + const jsdom = new JSDOM( + '
', + { + runScripts: 'dangerously', + }, + ); + document = jsdom.window.document; + container = document.getElementById('container'); + + buffer = ''; + hasErrored = false; + + writable = new Stream.PassThrough(); + writable.setEncoding('utf8'); + writable.on('data', chunk => { + buffer += chunk; + }); + writable.on('error', error => { + hasErrored = true; + fatalError = error; + }); + }); + + async function serverAct(callback) { + await callback(); + // Await one turn around the event loop. + // This assumes that we'll flush everything we have so far. + await new Promise(resolve => { + setImmediate(resolve); + }); + if (hasErrored) { + throw fatalError; + } + // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment. + // We also want to execute any scripts that are embedded. + // We assume that we have now received a proper fragment of HTML. + const bufferedContent = buffer; + buffer = ''; + const fakeBody = document.createElement('body'); + fakeBody.innerHTML = bufferedContent; + while (fakeBody.firstChild) { + const node = fakeBody.firstChild; + if (node.nodeName === 'SCRIPT') { + const script = document.createElement('script'); + script.textContent = node.textContent; + fakeBody.removeChild(node); + container.appendChild(script); + } else { + container.appendChild(node); + } + } + } + + function normalizeTreeIdForTesting(id) { + const [serverClientPrefix, base32, hookIndex] = id.split(':'); + if (serverClientPrefix === 'r') { + // Client ids aren't stable. For testing purposes, strip out the counter. + return ( + 'CLIENT_GENERATED_ID' + + (hookIndex !== undefined ? ` (${hookIndex})` : '') + ); + } + // Formats the tree id as a binary sequence, so it's easier to visualize + // the structure. + return ( + parseInt(base32, 32).toString(2) + + (hookIndex !== undefined ? ` (${hookIndex})` : '') + ); + } + + function DivWithId({children}) { + const id = normalizeTreeIdForTesting(useId()); + return
{children}
; + } + + test('basic example', async () => { + function App() { + return ( +
+
+ + +
+ +
+ ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + await clientAct(async () => { + ReactDOM.hydrateRoot(container, ); + }); + expect(container).toMatchInlineSnapshot(` +
+
+
+
+
+
+
+
+
+ `); + }); + + test('indirections', async () => { + function App() { + // There are no forks in this tree, but the parent and the child should + // have different ids. + return ( + +
+
+
+ +
+
+
+
+ ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + await clientAct(async () => { + ReactDOM.hydrateRoot(container, ); + }); + expect(container).toMatchInlineSnapshot(` +
+
+
+
+
+
+
+
+
+
+
+ `); + }); + + test('empty (null) children', async () => { + // We don't treat empty children different from non-empty ones, which means + // they get allocated a slot when generating ids. There's no inherent reason + // to do this; Fiber happens to allocate a fiber for null children that + // appear in a list, which is not ideal for performance. For the purposes + // of id generation, though, what matters is that Fizz and Fiber + // are consistent. + function App() { + return ( + <> + {null} + + {null} + + + ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + await clientAct(async () => { + ReactDOM.hydrateRoot(container, ); + }); + expect(container).toMatchInlineSnapshot(` +
+
+
+
+ `); + }); + + test('large ids', async () => { + // The component in this test outputs a recursive tree of nodes with ids, + // where the underlying binary representation is an alternating series of 1s + // and 0s. In other words, they are all of the form 101010101. + // + // Because we use base 32 encoding, the resulting id should consist of + // alternating 'a' (01010) and 'l' (10101) characters, except for the the + // 'R:' prefix, and the first character after that, which may not correspond + // to a complete set of 5 bits. + // + // Example: R:clalalalalalalala... + // + // We can use this pattern to test large ids that exceed the bitwise + // safe range (32 bits). The algorithm should theoretically support ids + // of any size. + + function Child({children}) { + const id = useId(); + return
{children}
; + } + + function App() { + let tree = ; + for (let i = 0; i < 50; i++) { + tree = ( + <> + + {tree} + + ); + } + return tree; + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + await clientAct(async () => { + ReactDOM.hydrateRoot(container, ); + }); + const divs = container.querySelectorAll('div'); + + // Confirm that every id matches the expected pattern + for (let i = 0; i < divs.length; i++) { + // Example: R:clalalalalalalala... + expect(divs[i].id).toMatch(/^R:.(((al)*a?)((la)*l?))*$/); + } + }); + + test('multiple ids in a single component', async () => { + function App() { + const id1 = useId(); + const id2 = useId(); + const id3 = useId(); + return `${id1}, ${id2}, ${id3}`; + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + await clientAct(async () => { + ReactDOM.hydrateRoot(container, ); + }); + // We append a suffix to the end of the id to distinguish them + expect(container).toMatchInlineSnapshot(` +
+ R:0, R:0:1, R:0:2 + +
+ `); + }); + + test('basic incremental hydration', async () => { + function App() { + return ( +
+ + + + + +
+ ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + await clientAct(async () => { + ReactDOM.hydrateRoot(container, ); + }); + expect(container).toMatchInlineSnapshot(` +
+
+ +
+
+ +
+
+
+ `); + }); + + test('inserting/deleting siblings outside a dehydrated Suspense boundary', async () => { + const span = React.createRef(null); + function App({swap}) { + // Note: Using a dynamic array so these are treated as insertions and + // deletions instead of updates, because Fiber currently allocates a node + // even for empty children. + const children = [ + , + swap ? : , + , + ]; + return ( + <> + {children} + + + + + + ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + const dehydratedSpan = container.getElementsByTagName('span')[0]; + await clientAct(async () => { + const root = ReactDOM.hydrateRoot(container, ); + expect(Scheduler).toFlushUntilNextPaint([]); + expect(container).toMatchInlineSnapshot(` +
+
+
+
+ +
+ + +
+ `); + + // The inner boundary hasn't hydrated yet + expect(span.current).toBe(null); + + // Swap B for C + root.render(); + }); + // The swap should not have caused a mismatch. + expect(container).toMatchInlineSnapshot(` +
+
+
+
+ +
+ + +
+ `); + // Should have hydrated successfully + expect(span.current).toBe(dehydratedSpan); + }); + + test('inserting/deleting siblings inside a dehydrated Suspense boundary', async () => { + const span = React.createRef(null); + function App({swap}) { + // Note: Using a dynamic array so these are treated as insertions and + // deletions instead of updates, because Fiber currently allocates a node + // even for empty children. + const children = [ + , + swap ? : , + , + ]; + return ( + + {children} + + + ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + const dehydratedSpan = container.getElementsByTagName('span')[0]; + await clientAct(async () => { + const root = ReactDOM.hydrateRoot(container, ); + expect(Scheduler).toFlushUntilNextPaint([]); + expect(container).toMatchInlineSnapshot(` +
+ +
+
+
+ + +
+ `); + + // The inner boundary hasn't hydrated yet + expect(span.current).toBe(null); + + // Swap B for C + root.render(); + }); + // The swap should not have caused a mismatch. + expect(container).toMatchInlineSnapshot(` +
+ +
+
+
+ + +
+ `); + // Should have hydrated successfully + expect(span.current).toBe(dehydratedSpan); + }); +}); diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index 168fd78f6103e..26f2dd00ee0c6 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -519,6 +519,10 @@ function useOpaqueIdentifier(): OpaqueIDType { ); } +function useId(): OpaqueIDType { + throw new Error('Not implemented.'); +} + function useCacheRefresh(): (?() => T, ?T) => void { throw new Error('Not implemented.'); } @@ -549,6 +553,7 @@ export const Dispatcher: DispatcherType = { useDeferredValue, useTransition, useOpaqueIdentifier, + useId, // Subscriptions are not setup in a server environment. useMutableSource, useSyncExternalStore, diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js index 9071edc24f7a2..658b1f0e7b799 100644 --- a/packages/react-reconciler/src/ReactChildFiber.new.js +++ b/packages/react-reconciler/src/ReactChildFiber.new.js @@ -13,7 +13,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane.new'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; -import {Placement, ChildDeletion} from './ReactFiberFlags'; +import {Placement, ChildDeletion, Forked} from './ReactFiberFlags'; import { getIteratorFn, REACT_ELEMENT_TYPE, @@ -40,6 +40,8 @@ import { import {emptyRefsObject} from './ReactFiberClassComponent.new'; import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.new'; import {StrictLegacyMode} from './ReactTypeOfMode'; +import {getIsHydrating} from './ReactFiberHydrationContext.new'; +import {pushTreeFork} from './ReactFiberTreeContext.new'; let didWarnAboutMaps; let didWarnAboutGenerators; @@ -334,7 +336,9 @@ function ChildReconciler(shouldTrackSideEffects) { ): number { newFiber.index = newIndex; if (!shouldTrackSideEffects) { - // Noop. + // During hydration, the useId algorithm needs to know which fibers are + // part of a list of children (arrays, iterators). + newFiber.flags |= Forked; return lastPlacedIndex; } const current = newFiber.alternate; @@ -823,6 +827,10 @@ function ChildReconciler(shouldTrackSideEffects) { if (newIdx === newChildren.length) { // We've reached the end of the new children. We can delete the rest. deleteRemainingChildren(returnFiber, oldFiber); + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } @@ -843,6 +851,10 @@ function ChildReconciler(shouldTrackSideEffects) { } previousNewFiber = newFiber; } + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } @@ -886,6 +898,10 @@ function ChildReconciler(shouldTrackSideEffects) { existingChildren.forEach(child => deleteChild(returnFiber, child)); } + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } @@ -1013,6 +1029,10 @@ function ChildReconciler(shouldTrackSideEffects) { if (step.done) { // We've reached the end of the new children. We can delete the rest. deleteRemainingChildren(returnFiber, oldFiber); + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } @@ -1033,6 +1053,10 @@ function ChildReconciler(shouldTrackSideEffects) { } previousNewFiber = newFiber; } + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } @@ -1076,6 +1100,10 @@ function ChildReconciler(shouldTrackSideEffects) { existingChildren.forEach(child => deleteChild(returnFiber, child)); } + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } diff --git a/packages/react-reconciler/src/ReactChildFiber.old.js b/packages/react-reconciler/src/ReactChildFiber.old.js index 0128ca8f36f3d..0ef3b301e95a7 100644 --- a/packages/react-reconciler/src/ReactChildFiber.old.js +++ b/packages/react-reconciler/src/ReactChildFiber.old.js @@ -13,7 +13,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane.old'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; -import {Placement, ChildDeletion} from './ReactFiberFlags'; +import {Placement, ChildDeletion, Forked} from './ReactFiberFlags'; import { getIteratorFn, REACT_ELEMENT_TYPE, @@ -40,6 +40,8 @@ import { import {emptyRefsObject} from './ReactFiberClassComponent.old'; import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.old'; import {StrictLegacyMode} from './ReactTypeOfMode'; +import {getIsHydrating} from './ReactFiberHydrationContext.old'; +import {pushTreeFork} from './ReactFiberTreeContext.old'; let didWarnAboutMaps; let didWarnAboutGenerators; @@ -334,7 +336,9 @@ function ChildReconciler(shouldTrackSideEffects) { ): number { newFiber.index = newIndex; if (!shouldTrackSideEffects) { - // Noop. + // During hydration, the useId algorithm needs to know which fibers are + // part of a list of children (arrays, iterators). + newFiber.flags |= Forked; return lastPlacedIndex; } const current = newFiber.alternate; @@ -823,6 +827,10 @@ function ChildReconciler(shouldTrackSideEffects) { if (newIdx === newChildren.length) { // We've reached the end of the new children. We can delete the rest. deleteRemainingChildren(returnFiber, oldFiber); + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } @@ -843,6 +851,10 @@ function ChildReconciler(shouldTrackSideEffects) { } previousNewFiber = newFiber; } + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } @@ -886,6 +898,10 @@ function ChildReconciler(shouldTrackSideEffects) { existingChildren.forEach(child => deleteChild(returnFiber, child)); } + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } @@ -1013,6 +1029,10 @@ function ChildReconciler(shouldTrackSideEffects) { if (step.done) { // We've reached the end of the new children. We can delete the rest. deleteRemainingChildren(returnFiber, oldFiber); + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } @@ -1033,6 +1053,10 @@ function ChildReconciler(shouldTrackSideEffects) { } previousNewFiber = newFiber; } + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } @@ -1076,6 +1100,10 @@ function ChildReconciler(shouldTrackSideEffects) { existingChildren.forEach(child => deleteChild(returnFiber, child)); } + if (getIsHydrating()) { + const numberOfForks = newIdx; + pushTreeFork(returnFiber, numberOfForks); + } return resultingFirstChild; } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 4fe648bc3e767..653ee9e1b4ea7 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -186,6 +186,7 @@ import { invalidateContextProvider, } from './ReactFiberContext.new'; import { + getIsHydrating, enterHydrationState, reenterHydrationStateFromDehydratedSuspenseInstance, resetHydrationState, @@ -235,6 +236,11 @@ import {createClassErrorUpdate} from './ReactFiberThrow.new'; import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.new'; import is from 'shared/objectIs'; import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.new'; +import { + getForksAtLevel, + isForkedChild, + pushTreeId, +} from './ReactFiberTreeContext.new'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -1757,6 +1763,7 @@ function mountIndeterminateComponent( } } } + reconcileChildren(null, workInProgress, value, renderLanes); if (__DEV__) { validateFunctionComponentInDev(workInProgress, Component); @@ -1845,6 +1852,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { const SUSPENDED_MARKER: SuspenseState = { dehydrated: null, + treeContext: null, retryLane: NoLane, }; @@ -2693,6 +2701,7 @@ function updateDehydratedSuspenseComponent( reenterHydrationStateFromDehydratedSuspenseInstance( workInProgress, suspenseInstance, + suspenseState.treeContext, ); const nextProps = workInProgress.pendingProps; const primaryChildren = nextProps.children; @@ -3675,6 +3684,21 @@ function beginWork( } } else { didReceiveUpdate = false; + + if (getIsHydrating() && isForkedChild(workInProgress)) { + // Check if this child belongs to a list of muliple children in + // its parent. + // + // In a true multi-threaded implementation, we would render children on + // parallel threads. This would represent the beginning of a new render + // thread for this subtree. + // + // We only use this for id generation during hydration, which is why the + // logic is located in this special branch. + const slotIndex = workInProgress.index; + const numberOfForks = getForksAtLevel(workInProgress); + pushTreeId(workInProgress, numberOfForks, slotIndex); + } } // Before entering the begin phase, clear pending update priority. diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index f116897a8661b..9833ef481af70 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -186,6 +186,7 @@ import { invalidateContextProvider, } from './ReactFiberContext.old'; import { + getIsHydrating, enterHydrationState, reenterHydrationStateFromDehydratedSuspenseInstance, resetHydrationState, @@ -235,6 +236,11 @@ import {createClassErrorUpdate} from './ReactFiberThrow.old'; import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.old'; import is from 'shared/objectIs'; import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.old'; +import { + getForksAtLevel, + isForkedChild, + pushTreeId, +} from './ReactFiberTreeContext.old'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -1757,6 +1763,7 @@ function mountIndeterminateComponent( } } } + reconcileChildren(null, workInProgress, value, renderLanes); if (__DEV__) { validateFunctionComponentInDev(workInProgress, Component); @@ -1845,6 +1852,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { const SUSPENDED_MARKER: SuspenseState = { dehydrated: null, + treeContext: null, retryLane: NoLane, }; @@ -2693,6 +2701,7 @@ function updateDehydratedSuspenseComponent( reenterHydrationStateFromDehydratedSuspenseInstance( workInProgress, suspenseInstance, + suspenseState.treeContext, ); const nextProps = workInProgress.pendingProps; const primaryChildren = nextProps.children; @@ -3675,6 +3684,21 @@ function beginWork( } } else { didReceiveUpdate = false; + + if (getIsHydrating() && isForkedChild(workInProgress)) { + // Check if this child belongs to a list of muliple children in + // its parent. + // + // In a true multi-threaded implementation, we would render children on + // parallel threads. This would represent the beginning of a new render + // thread for this subtree. + // + // We only use this for id generation during hydration, which is why the + // logic is located in this special branch. + const slotIndex = workInProgress.index; + const numberOfForks = getForksAtLevel(workInProgress); + pushTreeId(workInProgress, numberOfForks, slotIndex); + } } // Before entering the begin phase, clear pending update priority. diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index 20a7fc52db13a..feb38f00461a0 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -155,6 +155,7 @@ import { popRootCachePool, popCachePool, } from './ReactFiberCacheComponent.new'; +import {popTreeContext} from './ReactFiberTreeContext.new'; function markUpdate(workInProgress: Fiber) { // Tag the fiber with an update effect. This turns a Placement into @@ -822,7 +823,11 @@ function completeWork( renderLanes: Lanes, ): Fiber | null { const newProps = workInProgress.pendingProps; - + // Note: This intentionally doesn't check if we're hydrating because comparing + // to the current tree provider fiber is just as fast and less error-prone. + // Ideally we would have a special version of the work loop only + // for hydration. + popTreeContext(workInProgress); switch (workInProgress.tag) { case IndeterminateComponent: case LazyComponent: diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index 305359aef206e..ea4d71e8ba371 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -155,6 +155,7 @@ import { popRootCachePool, popCachePool, } from './ReactFiberCacheComponent.old'; +import {popTreeContext} from './ReactFiberTreeContext.old'; function markUpdate(workInProgress: Fiber) { // Tag the fiber with an update effect. This turns a Placement into @@ -822,7 +823,11 @@ function completeWork( renderLanes: Lanes, ): Fiber | null { const newProps = workInProgress.pendingProps; - + // Note: This intentionally doesn't check if we're hydrating because comparing + // to the current tree provider fiber is just as fast and less error-prone. + // Ideally we would have a special version of the work loop only + // for hydration. + popTreeContext(workInProgress); switch (workInProgress.tag) { case IndeterminateComponent: case LazyComponent: diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js index a82278222bf0a..805c4bed918e9 100644 --- a/packages/react-reconciler/src/ReactFiberFlags.js +++ b/packages/react-reconciler/src/ReactFiberFlags.js @@ -12,54 +12,55 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags'; export type Flags = number; // Don't change these two values. They're used by React Dev Tools. -export const NoFlags = /* */ 0b0000000000000000000000000; -export const PerformedWork = /* */ 0b0000000000000000000000001; +export const NoFlags = /* */ 0b00000000000000000000000000; +export const PerformedWork = /* */ 0b00000000000000000000000001; // You can change the rest (and add more). -export const Placement = /* */ 0b0000000000000000000000010; -export const Update = /* */ 0b0000000000000000000000100; +export const Placement = /* */ 0b00000000000000000000000010; +export const Update = /* */ 0b00000000000000000000000100; export const PlacementAndUpdate = /* */ Placement | Update; -export const Deletion = /* */ 0b0000000000000000000001000; -export const ChildDeletion = /* */ 0b0000000000000000000010000; -export const ContentReset = /* */ 0b0000000000000000000100000; -export const Callback = /* */ 0b0000000000000000001000000; -export const DidCapture = /* */ 0b0000000000000000010000000; -export const ForceClientRender = /* */ 0b0000000000000000100000000; -export const Ref = /* */ 0b0000000000000001000000000; -export const Snapshot = /* */ 0b0000000000000010000000000; -export const Passive = /* */ 0b0000000000000100000000000; -export const Hydrating = /* */ 0b0000000000001000000000000; +export const Deletion = /* */ 0b00000000000000000000001000; +export const ChildDeletion = /* */ 0b00000000000000000000010000; +export const ContentReset = /* */ 0b00000000000000000000100000; +export const Callback = /* */ 0b00000000000000000001000000; +export const DidCapture = /* */ 0b00000000000000000010000000; +export const ForceClientRender = /* */ 0b00000000000000000100000000; +export const Ref = /* */ 0b00000000000000001000000000; +export const Snapshot = /* */ 0b00000000000000010000000000; +export const Passive = /* */ 0b00000000000000100000000000; +export const Hydrating = /* */ 0b00000000000001000000000000; export const HydratingAndUpdate = /* */ Hydrating | Update; -export const Visibility = /* */ 0b0000000000010000000000000; -export const StoreConsistency = /* */ 0b0000000000100000000000000; +export const Visibility = /* */ 0b00000000000010000000000000; +export const StoreConsistency = /* */ 0b00000000000100000000000000; export const LifecycleEffectMask = Passive | Update | Callback | Ref | Snapshot | StoreConsistency; // Union of all commit flags (flags with the lifetime of a particular commit) -export const HostEffectMask = /* */ 0b0000000000111111111111111; +export const HostEffectMask = /* */ 0b00000000000111111111111111; // These are not really side effects, but we still reuse this field. -export const Incomplete = /* */ 0b0000000001000000000000000; -export const ShouldCapture = /* */ 0b0000000010000000000000000; -export const ForceUpdateForLegacySuspense = /* */ 0b0000000100000000000000000; -export const DidPropagateContext = /* */ 0b0000001000000000000000000; -export const NeedsPropagation = /* */ 0b0000010000000000000000000; +export const Incomplete = /* */ 0b00000000001000000000000000; +export const ShouldCapture = /* */ 0b00000000010000000000000000; +export const ForceUpdateForLegacySuspense = /* */ 0b00000000100000000000000000; +export const DidPropagateContext = /* */ 0b00000001000000000000000000; +export const NeedsPropagation = /* */ 0b00000010000000000000000000; +export const Forked = /* */ 0b00000100000000000000000000; // Static tags describe aspects of a fiber that are not specific to a render, // e.g. a fiber uses a passive effect (even if there are no updates on this particular render). // This enables us to defer more work in the unmount case, // since we can defer traversing the tree during layout to look for Passive effects, // and instead rely on the static flag as a signal that there may be cleanup work. -export const RefStatic = /* */ 0b0000100000000000000000000; -export const LayoutStatic = /* */ 0b0001000000000000000000000; -export const PassiveStatic = /* */ 0b0010000000000000000000000; +export const RefStatic = /* */ 0b00001000000000000000000000; +export const LayoutStatic = /* */ 0b00010000000000000000000000; +export const PassiveStatic = /* */ 0b00100000000000000000000000; // These flags allow us to traverse to fibers that have effects on mount // without traversing the entire tree after every commit for // double invoking -export const MountLayoutDev = /* */ 0b0100000000000000000000000; -export const MountPassiveDev = /* */ 0b1000000000000000000000000; +export const MountLayoutDev = /* */ 0b01000000000000000000000000; +export const MountPassiveDev = /* */ 0b10000000000000000000000000; // Groups of flags that are used in the commit phase to skip over trees that // don't contain effects, by checking subtreeFlags. diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 732f7d71a7e71..a1d7009a85a43 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -117,6 +117,7 @@ import { } from './ReactUpdateQueue.new'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; +import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.new'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -203,6 +204,12 @@ let didScheduleRenderPhaseUpdate: boolean = false; // TODO: Maybe there's some way to consolidate this with // `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`. let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false; +// Counts the number of useId hooks in this component. +let localIdCounter: number = 0; +// Used for ids that are generated completely client-side (i.e. not during +// hydration). This counter is global, so client ids are not stable across +// render attempts. +let globalClientIdCounter: number = 0; const RE_RENDER_LIMIT = 25; @@ -396,6 +403,7 @@ export function renderWithHooks( // workInProgressHook = null; // didScheduleRenderPhaseUpdate = false; + // localIdCounter = 0; // TODO Warn if no hooks are used at all during mount, then some are used during update. // Currently we will identify the update render as a mount because memoizedState === null. @@ -543,6 +551,21 @@ export function renderWithHooks( } } + if (localIdCounter !== 0) { + localIdCounter = 0; + if (getIsHydrating()) { + // This component materialized an id. This will affect any ids that appear + // in its children. + const returnFiber = workInProgress.return; + if (returnFiber !== null) { + const numberOfForks = 1; + const slotIndex = 0; + pushTreeFork(workInProgress, numberOfForks); + pushTreeId(workInProgress, numberOfForks, slotIndex); + } + } + } + return children; } @@ -612,6 +635,7 @@ export function resetHooksAfterThrow(): void { } didScheduleRenderPhaseUpdateDuringThisPass = false; + localIdCounter = 0; } function mountWorkInProgressHook(): Hook { @@ -2109,6 +2133,39 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void { return id; } +function mountId(): string { + const hook = mountWorkInProgressHook(); + + let id; + if (getIsHydrating()) { + const treeId = getTreeId(); + + // Use a captial R prefix for server-generated ids. + id = 'R:' + treeId; + + // Unless this is the first id at this level, append a number at the end + // that represents the position of this useId hook among all the useId + // hooks for this fiber. + const localId = localIdCounter++; + if (localId > 0) { + id += ':' + localId.toString(32); + } + } else { + // Use a lowercase r prefix for client-generated ids. + const globalClientId = globalClientIdCounter++; + id = 'r:' + globalClientId.toString(32); + } + + hook.memoizedState = id; + return id; +} + +function updateId(): string { + const hook = updateWorkInProgressHook(); + const id: string = hook.memoizedState; + return id; +} + function mountRefresh() { const hook = mountWorkInProgressHook(); const refresh = (hook.memoizedState = refreshCache.bind( @@ -2425,6 +2482,7 @@ export const ContextOnlyDispatcher: Dispatcher = { useMutableSource: throwInvalidHookError, useSyncExternalStore: throwInvalidHookError, useOpaqueIdentifier: throwInvalidHookError, + useId: throwInvalidHookError, unstable_isNewReconciler: enableNewReconciler, }; @@ -2453,6 +2511,7 @@ const HooksDispatcherOnMount: Dispatcher = { useMutableSource: mountMutableSource, useSyncExternalStore: mountSyncExternalStore, useOpaqueIdentifier: mountOpaqueIdentifier, + useId: mountId, unstable_isNewReconciler: enableNewReconciler, }; @@ -2481,6 +2540,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { useMutableSource: updateMutableSource, useSyncExternalStore: updateSyncExternalStore, useOpaqueIdentifier: updateOpaqueIdentifier, + useId: updateId, unstable_isNewReconciler: enableNewReconciler, }; @@ -2509,6 +2569,7 @@ const HooksDispatcherOnRerender: Dispatcher = { useMutableSource: updateMutableSource, useSyncExternalStore: mountSyncExternalStore, useOpaqueIdentifier: rerenderOpaqueIdentifier, + useId: updateId, unstable_isNewReconciler: enableNewReconciler, }; @@ -2680,6 +2741,11 @@ if (__DEV__) { mountHookTypesDev(); return mountOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + mountHookTypesDev(); + return mountId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -2822,6 +2888,11 @@ if (__DEV__) { updateHookTypesDev(); return mountOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + updateHookTypesDev(); + return mountId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -2964,6 +3035,11 @@ if (__DEV__) { updateHookTypesDev(); return updateOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + updateHookTypesDev(); + return updateId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -3107,6 +3183,11 @@ if (__DEV__) { updateHookTypesDev(); return rerenderOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + updateHookTypesDev(); + return updateId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -3266,6 +3347,12 @@ if (__DEV__) { mountHookTypesDev(); return mountOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -3425,6 +3512,12 @@ if (__DEV__) { updateHookTypesDev(); return updateOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -3585,6 +3678,12 @@ if (__DEV__) { updateHookTypesDev(); return rerenderOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateId(); + }, unstable_isNewReconciler: enableNewReconciler, }; diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index b78f24e8b47f8..167698271dbac 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -117,6 +117,7 @@ import { } from './ReactUpdateQueue.old'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; +import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.old'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -203,6 +204,12 @@ let didScheduleRenderPhaseUpdate: boolean = false; // TODO: Maybe there's some way to consolidate this with // `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`. let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false; +// Counts the number of useId hooks in this component. +let localIdCounter: number = 0; +// Used for ids that are generated completely client-side (i.e. not during +// hydration). This counter is global, so client ids are not stable across +// render attempts. +let globalClientIdCounter: number = 0; const RE_RENDER_LIMIT = 25; @@ -396,6 +403,7 @@ export function renderWithHooks( // workInProgressHook = null; // didScheduleRenderPhaseUpdate = false; + // localIdCounter = 0; // TODO Warn if no hooks are used at all during mount, then some are used during update. // Currently we will identify the update render as a mount because memoizedState === null. @@ -543,6 +551,21 @@ export function renderWithHooks( } } + if (localIdCounter !== 0) { + localIdCounter = 0; + if (getIsHydrating()) { + // This component materialized an id. This will affect any ids that appear + // in its children. + const returnFiber = workInProgress.return; + if (returnFiber !== null) { + const numberOfForks = 1; + const slotIndex = 0; + pushTreeFork(workInProgress, numberOfForks); + pushTreeId(workInProgress, numberOfForks, slotIndex); + } + } + } + return children; } @@ -612,6 +635,7 @@ export function resetHooksAfterThrow(): void { } didScheduleRenderPhaseUpdateDuringThisPass = false; + localIdCounter = 0; } function mountWorkInProgressHook(): Hook { @@ -2109,6 +2133,39 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void { return id; } +function mountId(): string { + const hook = mountWorkInProgressHook(); + + let id; + if (getIsHydrating()) { + const treeId = getTreeId(); + + // Use a captial R prefix for server-generated ids. + id = 'R:' + treeId; + + // Unless this is the first id at this level, append a number at the end + // that represents the position of this useId hook among all the useId + // hooks for this fiber. + const localId = localIdCounter++; + if (localId > 0) { + id += ':' + localId.toString(32); + } + } else { + // Use a lowercase r prefix for client-generated ids. + const globalClientId = globalClientIdCounter++; + id = 'r:' + globalClientId.toString(32); + } + + hook.memoizedState = id; + return id; +} + +function updateId(): string { + const hook = updateWorkInProgressHook(); + const id: string = hook.memoizedState; + return id; +} + function mountRefresh() { const hook = mountWorkInProgressHook(); const refresh = (hook.memoizedState = refreshCache.bind( @@ -2425,6 +2482,7 @@ export const ContextOnlyDispatcher: Dispatcher = { useMutableSource: throwInvalidHookError, useSyncExternalStore: throwInvalidHookError, useOpaqueIdentifier: throwInvalidHookError, + useId: throwInvalidHookError, unstable_isNewReconciler: enableNewReconciler, }; @@ -2453,6 +2511,7 @@ const HooksDispatcherOnMount: Dispatcher = { useMutableSource: mountMutableSource, useSyncExternalStore: mountSyncExternalStore, useOpaqueIdentifier: mountOpaqueIdentifier, + useId: mountId, unstable_isNewReconciler: enableNewReconciler, }; @@ -2481,6 +2540,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { useMutableSource: updateMutableSource, useSyncExternalStore: updateSyncExternalStore, useOpaqueIdentifier: updateOpaqueIdentifier, + useId: updateId, unstable_isNewReconciler: enableNewReconciler, }; @@ -2509,6 +2569,7 @@ const HooksDispatcherOnRerender: Dispatcher = { useMutableSource: updateMutableSource, useSyncExternalStore: mountSyncExternalStore, useOpaqueIdentifier: rerenderOpaqueIdentifier, + useId: updateId, unstable_isNewReconciler: enableNewReconciler, }; @@ -2680,6 +2741,11 @@ if (__DEV__) { mountHookTypesDev(); return mountOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + mountHookTypesDev(); + return mountId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -2822,6 +2888,11 @@ if (__DEV__) { updateHookTypesDev(); return mountOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + updateHookTypesDev(); + return mountId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -2964,6 +3035,11 @@ if (__DEV__) { updateHookTypesDev(); return updateOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + updateHookTypesDev(); + return updateId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -3107,6 +3183,11 @@ if (__DEV__) { updateHookTypesDev(); return rerenderOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + updateHookTypesDev(); + return updateId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -3266,6 +3347,12 @@ if (__DEV__) { mountHookTypesDev(); return mountOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -3425,6 +3512,12 @@ if (__DEV__) { updateHookTypesDev(); return updateOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateId(); + }, unstable_isNewReconciler: enableNewReconciler, }; @@ -3585,6 +3678,12 @@ if (__DEV__) { updateHookTypesDev(); return rerenderOpaqueIdentifier(); }, + useId(): string { + currentHookNameInDev = 'useId'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateId(); + }, unstable_isNewReconciler: enableNewReconciler, }; diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index 7275f1663cad8..eabc5e43116bb 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -17,6 +17,7 @@ import type { HostContext, } from './ReactFiberHostConfig'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; +import type {TreeContext} from './ReactFiberTreeContext.new'; import { HostComponent, @@ -62,6 +63,10 @@ import { } from './ReactFiberHostConfig'; import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; import {OffscreenLane} from './ReactFiberLane.new'; +import { + getSuspendedTreeContext, + restoreSuspendedTreeContext, +} from './ReactFiberTreeContext.new'; // The deepest Fiber on the stack involved in a hydration context. // This may have been an insertion or a hydration. @@ -96,6 +101,7 @@ function enterHydrationState(fiber: Fiber): boolean { function reenterHydrationStateFromDehydratedSuspenseInstance( fiber: Fiber, suspenseInstance: SuspenseInstance, + treeContext: TreeContext | null, ): boolean { if (!supportsHydration) { return false; @@ -105,6 +111,9 @@ function reenterHydrationStateFromDehydratedSuspenseInstance( ); hydrationParentFiber = fiber; isHydrating = true; + if (treeContext !== null) { + restoreSuspendedTreeContext(fiber, treeContext); + } return true; } @@ -287,6 +296,7 @@ function tryHydrate(fiber, nextInstance) { if (suspenseInstance !== null) { const suspenseState: SuspenseState = { dehydrated: suspenseInstance, + treeContext: getSuspendedTreeContext(), retryLane: OffscreenLane, }; fiber.memoizedState = suspenseState; diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js index 654de3f9a2894..48e60581e0f28 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js @@ -17,6 +17,7 @@ import type { HostContext, } from './ReactFiberHostConfig'; import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; +import type {TreeContext} from './ReactFiberTreeContext.old'; import { HostComponent, @@ -62,6 +63,10 @@ import { } from './ReactFiberHostConfig'; import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; import {OffscreenLane} from './ReactFiberLane.old'; +import { + getSuspendedTreeContext, + restoreSuspendedTreeContext, +} from './ReactFiberTreeContext.old'; // The deepest Fiber on the stack involved in a hydration context. // This may have been an insertion or a hydration. @@ -96,6 +101,7 @@ function enterHydrationState(fiber: Fiber): boolean { function reenterHydrationStateFromDehydratedSuspenseInstance( fiber: Fiber, suspenseInstance: SuspenseInstance, + treeContext: TreeContext | null, ): boolean { if (!supportsHydration) { return false; @@ -105,6 +111,9 @@ function reenterHydrationStateFromDehydratedSuspenseInstance( ); hydrationParentFiber = fiber; isHydrating = true; + if (treeContext !== null) { + restoreSuspendedTreeContext(fiber, treeContext); + } return true; } @@ -287,6 +296,7 @@ function tryHydrate(fiber, nextInstance) { if (suspenseInstance !== null) { const suspenseState: SuspenseState = { dehydrated: suspenseInstance, + treeContext: getSuspendedTreeContext(), retryLane: OffscreenLane, }; fiber.memoizedState = suspenseState; diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index c1f34e1052fc4..7e1461c7a7226 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -23,6 +23,7 @@ import { } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; +import {clz32} from './clz32'; // Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler. // If those values are changed that package should be rebuilt and redeployed. @@ -791,17 +792,3 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) { lanes &= ~lane; } } - -const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback; - -// Count leading zeros. Only used on lanes, so assume input is an integer. -// Based on: -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 -const log = Math.log; -const LN2 = Math.LN2; -function clz32Fallback(lanes: Lanes | Lane) { - if (lanes === 0) { - return 32; - } - return (31 - ((log(lanes) / LN2) | 0)) | 0; -} diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index c81191f6a07e5..6b4be15e649f1 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -23,6 +23,7 @@ import { } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; +import {clz32} from './clz32'; // Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler. // If those values are changed that package should be rebuilt and redeployed. @@ -791,17 +792,3 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) { lanes &= ~lane; } } - -const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback; - -// Count leading zeros. Only used on lanes, so assume input is an integer. -// Based on: -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 -const log = Math.log; -const LN2 = Math.LN2; -function clz32Fallback(lanes: Lanes | Lane) { - if (lanes === 0) { - return 32; - } - return (31 - ((log(lanes) / LN2) | 0)) | 0; -} diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js index 5ad7ae650249a..9dbaf7fb76efd 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js @@ -11,6 +11,8 @@ import type {ReactNodeList, Wakeable} from 'shared/ReactTypes'; import type {Fiber} from './ReactInternalTypes'; import type {SuspenseInstance} from './ReactFiberHostConfig'; import type {Lane} from './ReactFiberLane.new'; +import type {TreeContext} from './ReactFiberTreeContext.new'; + import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags'; import {NoFlags, DidCapture} from './ReactFiberFlags'; import { @@ -40,6 +42,7 @@ export type SuspenseState = {| // here to indicate that it is dehydrated (flag) and for quick access // to check things like isSuspenseInstancePending. dehydrated: null | SuspenseInstance, + treeContext: null | TreeContext, // Represents the lane we should attempt to hydrate a dehydrated boundary at. // OffscreenLane is the default for dehydrated boundaries. // NoLane is the default for normal boundaries, which turns into "normal" pri. diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js index 51bef1df3a568..726f0ca52005f 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js @@ -11,6 +11,8 @@ import type {ReactNodeList, Wakeable} from 'shared/ReactTypes'; import type {Fiber} from './ReactInternalTypes'; import type {SuspenseInstance} from './ReactFiberHostConfig'; import type {Lane} from './ReactFiberLane.old'; +import type {TreeContext} from './ReactFiberTreeContext.old'; + import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags'; import {NoFlags, DidCapture} from './ReactFiberFlags'; import { @@ -40,6 +42,7 @@ export type SuspenseState = {| // here to indicate that it is dehydrated (flag) and for quick access // to check things like isSuspenseInstancePending. dehydrated: null | SuspenseInstance, + treeContext: null | TreeContext, // Represents the lane we should attempt to hydrate a dehydrated boundary at. // OffscreenLane is the default for dehydrated boundaries. // NoLane is the default for normal boundaries, which turns into "normal" pri. diff --git a/packages/react-reconciler/src/ReactFiberTreeContext.new.js b/packages/react-reconciler/src/ReactFiberTreeContext.new.js new file mode 100644 index 0000000000000..0725ba577e647 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberTreeContext.new.js @@ -0,0 +1,273 @@ +/** + * 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 + */ + +// Ids are base 32 strings whose binary representation corresponds to the +// position of a node in a tree. + +// Every time the tree forks into multiple children, we add additional bits to +// the left of the sequence that represent the position of the child within the +// current level of children. +// +// 00101 00010001011010101 +// ╰─┬─╯ ╰───────┬───────╯ +// Fork 5 of 20 Parent id +// +// The leading 0s are important. In the above example, you only need 3 bits to +// represent slot 5. However, you need 5 bits to represent all the forks at +// the current level, so we must account for the empty bits at the end. +// +// For this same reason, slots are 1-indexed instead of 0-indexed. Otherwise, +// the zeroth id at a level would be indistinguishable from its parent. +// +// If a node has only one child, and does not materialize an id (i.e. does not +// contain a useId hook), then we don't need to allocate any space in the +// sequence. It's treated as a transparent indirection. For example, these two +// trees produce the same ids: +// +// <> <> +// +// +// +// +// +// +// However, we cannot skip any node that materializes an id. Otherwise, a parent +// id that does not fork would be indistinguishable from its child id. For +// example, this tree does not fork, but the parent and child must have +// different ids. +// +// +// +// +// +// To handle this scenario, every time we materialize an id, we allocate a +// new level with a single slot. You can think of this as a fork with only one +// prong, or an array of children with length 1. +// +// It's possible for the the size of the sequence to exceed 32 bits, the max +// size for bitwise operations. When this happens, we make more room by +// converting the right part of the id to a string and storing it in an overflow +// variable. We use a base 32 string representation, because 32 is the largest +// power of 2 that is supported by toString(). We want the base to be large so +// that the resulting ids are compact, and we want the base to be a power of 2 +// because every log2(base) bits corresponds to a single character, i.e. every +// log2(32) = 5 bits. That means we can lop bits off the end 5 at a time without +// affecting the final result. + +import {getIsHydrating} from './ReactFiberHydrationContext.new'; +import {clz32} from './clz32'; +import {Forked, NoFlags} from './ReactFiberFlags'; + +export type TreeContext = { + id: number, + overflow: string, +}; + +// TODO: Use the unified fiber stack module instead of this local one? +// Intentionally not using it yet to derisk the initial implementation, because +// the way we push/pop these values is a bit unusual. If there's a mistake, I'd +// rather the ids be wrong than crash the whole reconciler. +const forkStack: Array = []; +let forkStackIndex: number = 0; +let treeForkProvider: Fiber | null = null; +let treeForkCount: number = 0; + +const idStack: Array = []; +let idStackIndex: number = 0; +let treeContextProvider: Fiber | null = null; +let treeContextId: number = 1; +let treeContextOverflow: string = ''; + +export function isForkedChild(workInProgress: Fiber): boolean { + warnIfNotHydrating(); + return (workInProgress.flags & Forked) !== NoFlags; +} + +export function getForksAtLevel(workInProgress: Fiber): number { + warnIfNotHydrating(); + return treeForkCount; +} + +export function getTreeId(): string { + const overflow = treeContextOverflow; + const idWithLeadingBit = treeContextId; + const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit); + return id.toString(32) + overflow; +} + +export function pushTreeFork( + workInProgress: Fiber, + totalChildren: number, +): void { + // This is called right after we reconcile an array (or iterator) of child + // fibers, because that's the only place where we know how many children in + // the whole set without doing extra work later, or storing addtional + // information on the fiber. + // + // That's why this function is separate from pushTreeId — it's called during + // the render phase of the fork parent, not the child, which is where we push + // the other context values. + // + // In the Fizz implementation this is much simpler because the child is + // rendered in the same callstack as the parent. + // + // It might be better to just add a `forks` field to the Fiber type. It would + // make this module simpler. + + warnIfNotHydrating(); + + forkStack[forkStackIndex++] = treeForkCount; + forkStack[forkStackIndex++] = treeForkProvider; + + treeForkProvider = workInProgress; + treeForkCount = totalChildren; +} + +export function pushTreeId( + workInProgress: Fiber, + totalChildren: number, + index: number, +) { + warnIfNotHydrating(); + + idStack[idStackIndex++] = treeContextId; + idStack[idStackIndex++] = treeContextOverflow; + idStack[idStackIndex++] = treeContextProvider; + + treeContextProvider = workInProgress; + + const baseIdWithLeadingBit = treeContextId; + const baseOverflow = treeContextOverflow; + + // The leftmost 1 marks the end of the sequence, non-inclusive. It's not part + // of the id; we use it to account for leading 0s. + const baseLength = getBitLength(baseIdWithLeadingBit) - 1; + const baseId = baseIdWithLeadingBit & ~(1 << baseLength); + + const slot = index + 1; + const length = getBitLength(totalChildren) + baseLength; + + // 30 is the max length we can store without overflowing, taking into + // consideration the leading 1 we use to mark the end of the sequence. + if (length > 30) { + // We overflowed the bitwise-safe range. Fall back to slower algorithm. + // This branch assumes the length of the base id is greater than 5; it won't + // work for smaller ids, because you need 5 bits per character. + // + // We encode the id in multiple steps: first the base id, then the + // remaining digits. + // + // Each 5 bit sequence corresponds to a single base 32 character. So for + // example, if the current id is 23 bits long, we can convert 20 of those + // bits into a string of 4 characters, with 3 bits left over. + // + // First calculate how many bits in the base id represent a complete + // sequence of characters. + const numberOfOverflowBits = baseLength - (baseLength % 5); + + // Then create a bitmask that selects only those bits. + const newOverflowBits = (1 << numberOfOverflowBits) - 1; + + // Select the bits, and convert them to a base 32 string. + const newOverflow = (baseId & newOverflowBits).toString(32); + + // Now we can remove those bits from the base id. + const restOfBaseId = baseId >> numberOfOverflowBits; + const restOfBaseLength = baseLength - numberOfOverflowBits; + + // Finally, encode the rest of the bits using the normal algorithm. Because + // we made more room, this time it won't overflow. + const restOfLength = getBitLength(totalChildren) + restOfBaseLength; + const restOfNewBits = slot << restOfBaseLength; + const id = restOfNewBits | restOfBaseId; + const overflow = newOverflow + baseOverflow; + + treeContextId = (1 << restOfLength) | id; + treeContextOverflow = overflow; + } else { + // Normal path + const newBits = slot << baseLength; + const id = newBits | baseId; + const overflow = baseOverflow; + + treeContextId = (1 << length) | id; + treeContextOverflow = overflow; + } +} + +function getBitLength(number: number): number { + return 32 - clz32(number); +} + +function getLeadingBit(id: number) { + return 1 << (getBitLength(id) - 1); +} + +export function popTreeContext(workInProgress: Fiber) { + // Restore the previous values. + + // This is a bit more complicated than other context-like modules in Fiber + // because the same Fiber may appear on the stack multiple times and for + // different reasons. We have to keep popping until the work-in-progress is + // no longer at the top of the stack. + + while (workInProgress === treeForkProvider) { + treeForkProvider = forkStack[--forkStackIndex]; + forkStack[forkStackIndex] = null; + treeForkCount = forkStack[--forkStackIndex]; + forkStack[forkStackIndex] = null; + } + + while (workInProgress === treeContextProvider) { + treeContextProvider = idStack[--idStackIndex]; + idStack[idStackIndex] = null; + treeContextOverflow = idStack[--idStackIndex]; + idStack[idStackIndex] = null; + treeContextId = idStack[--idStackIndex]; + idStack[idStackIndex] = null; + } +} + +export function getSuspendedTreeContext(): TreeContext | null { + warnIfNotHydrating(); + if (treeContextProvider !== null) { + return { + id: treeContextId, + overflow: treeContextOverflow, + }; + } else { + return null; + } +} + +export function restoreSuspendedTreeContext( + workInProgress: Fiber, + suspendedContext: TreeContext, +) { + warnIfNotHydrating(); + + idStack[idStackIndex++] = treeContextId; + idStack[idStackIndex++] = treeContextOverflow; + idStack[idStackIndex++] = treeContextProvider; + + treeContextId = suspendedContext.id; + treeContextOverflow = suspendedContext.overflow; + treeContextProvider = workInProgress; +} + +function warnIfNotHydrating() { + if (__DEV__) { + if (!getIsHydrating()) { + console.error( + 'Expected to be hydrating. This is a bug in React. Please file ' + + 'an issue.', + ); + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberTreeContext.old.js b/packages/react-reconciler/src/ReactFiberTreeContext.old.js new file mode 100644 index 0000000000000..a4ba3c3ddb931 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberTreeContext.old.js @@ -0,0 +1,273 @@ +/** + * 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 + */ + +// Ids are base 32 strings whose binary representation corresponds to the +// position of a node in a tree. + +// Every time the tree forks into multiple children, we add additional bits to +// the left of the sequence that represent the position of the child within the +// current level of children. +// +// 00101 00010001011010101 +// ╰─┬─╯ ╰───────┬───────╯ +// Fork 5 of 20 Parent id +// +// The leading 0s are important. In the above example, you only need 3 bits to +// represent slot 5. However, you need 5 bits to represent all the forks at +// the current level, so we must account for the empty bits at the end. +// +// For this same reason, slots are 1-indexed instead of 0-indexed. Otherwise, +// the zeroth id at a level would be indistinguishable from its parent. +// +// If a node has only one child, and does not materialize an id (i.e. does not +// contain a useId hook), then we don't need to allocate any space in the +// sequence. It's treated as a transparent indirection. For example, these two +// trees produce the same ids: +// +// <> <> +// +// +// +// +// +// +// However, we cannot skip any node that materializes an id. Otherwise, a parent +// id that does not fork would be indistinguishable from its child id. For +// example, this tree does not fork, but the parent and child must have +// different ids. +// +// +// +// +// +// To handle this scenario, every time we materialize an id, we allocate a +// new level with a single slot. You can think of this as a fork with only one +// prong, or an array of children with length 1. +// +// It's possible for the the size of the sequence to exceed 32 bits, the max +// size for bitwise operations. When this happens, we make more room by +// converting the right part of the id to a string and storing it in an overflow +// variable. We use a base 32 string representation, because 32 is the largest +// power of 2 that is supported by toString(). We want the base to be large so +// that the resulting ids are compact, and we want the base to be a power of 2 +// because every log2(base) bits corresponds to a single character, i.e. every +// log2(32) = 5 bits. That means we can lop bits off the end 5 at a time without +// affecting the final result. + +import {getIsHydrating} from './ReactFiberHydrationContext.old'; +import {clz32} from './clz32'; +import {Forked, NoFlags} from './ReactFiberFlags'; + +export type TreeContext = { + id: number, + overflow: string, +}; + +// TODO: Use the unified fiber stack module instead of this local one? +// Intentionally not using it yet to derisk the initial implementation, because +// the way we push/pop these values is a bit unusual. If there's a mistake, I'd +// rather the ids be wrong than crash the whole reconciler. +const forkStack: Array = []; +let forkStackIndex: number = 0; +let treeForkProvider: Fiber | null = null; +let treeForkCount: number = 0; + +const idStack: Array = []; +let idStackIndex: number = 0; +let treeContextProvider: Fiber | null = null; +let treeContextId: number = 1; +let treeContextOverflow: string = ''; + +export function isForkedChild(workInProgress: Fiber): boolean { + warnIfNotHydrating(); + return (workInProgress.flags & Forked) !== NoFlags; +} + +export function getForksAtLevel(workInProgress: Fiber): number { + warnIfNotHydrating(); + return treeForkCount; +} + +export function getTreeId(): string { + const overflow = treeContextOverflow; + const idWithLeadingBit = treeContextId; + const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit); + return id.toString(32) + overflow; +} + +export function pushTreeFork( + workInProgress: Fiber, + totalChildren: number, +): void { + // This is called right after we reconcile an array (or iterator) of child + // fibers, because that's the only place where we know how many children in + // the whole set without doing extra work later, or storing addtional + // information on the fiber. + // + // That's why this function is separate from pushTreeId — it's called during + // the render phase of the fork parent, not the child, which is where we push + // the other context values. + // + // In the Fizz implementation this is much simpler because the child is + // rendered in the same callstack as the parent. + // + // It might be better to just add a `forks` field to the Fiber type. It would + // make this module simpler. + + warnIfNotHydrating(); + + forkStack[forkStackIndex++] = treeForkCount; + forkStack[forkStackIndex++] = treeForkProvider; + + treeForkProvider = workInProgress; + treeForkCount = totalChildren; +} + +export function pushTreeId( + workInProgress: Fiber, + totalChildren: number, + index: number, +) { + warnIfNotHydrating(); + + idStack[idStackIndex++] = treeContextId; + idStack[idStackIndex++] = treeContextOverflow; + idStack[idStackIndex++] = treeContextProvider; + + treeContextProvider = workInProgress; + + const baseIdWithLeadingBit = treeContextId; + const baseOverflow = treeContextOverflow; + + // The leftmost 1 marks the end of the sequence, non-inclusive. It's not part + // of the id; we use it to account for leading 0s. + const baseLength = getBitLength(baseIdWithLeadingBit) - 1; + const baseId = baseIdWithLeadingBit & ~(1 << baseLength); + + const slot = index + 1; + const length = getBitLength(totalChildren) + baseLength; + + // 30 is the max length we can store without overflowing, taking into + // consideration the leading 1 we use to mark the end of the sequence. + if (length > 30) { + // We overflowed the bitwise-safe range. Fall back to slower algorithm. + // This branch assumes the length of the base id is greater than 5; it won't + // work for smaller ids, because you need 5 bits per character. + // + // We encode the id in multiple steps: first the base id, then the + // remaining digits. + // + // Each 5 bit sequence corresponds to a single base 32 character. So for + // example, if the current id is 23 bits long, we can convert 20 of those + // bits into a string of 4 characters, with 3 bits left over. + // + // First calculate how many bits in the base id represent a complete + // sequence of characters. + const numberOfOverflowBits = baseLength - (baseLength % 5); + + // Then create a bitmask that selects only those bits. + const newOverflowBits = (1 << numberOfOverflowBits) - 1; + + // Select the bits, and convert them to a base 32 string. + const newOverflow = (baseId & newOverflowBits).toString(32); + + // Now we can remove those bits from the base id. + const restOfBaseId = baseId >> numberOfOverflowBits; + const restOfBaseLength = baseLength - numberOfOverflowBits; + + // Finally, encode the rest of the bits using the normal algorithm. Because + // we made more room, this time it won't overflow. + const restOfLength = getBitLength(totalChildren) + restOfBaseLength; + const restOfNewBits = slot << restOfBaseLength; + const id = restOfNewBits | restOfBaseId; + const overflow = newOverflow + baseOverflow; + + treeContextId = (1 << restOfLength) | id; + treeContextOverflow = overflow; + } else { + // Normal path + const newBits = slot << baseLength; + const id = newBits | baseId; + const overflow = baseOverflow; + + treeContextId = (1 << length) | id; + treeContextOverflow = overflow; + } +} + +function getBitLength(number: number): number { + return 32 - clz32(number); +} + +function getLeadingBit(id: number) { + return 1 << (getBitLength(id) - 1); +} + +export function popTreeContext(workInProgress: Fiber) { + // Restore the previous values. + + // This is a bit more complicated than other context-like modules in Fiber + // because the same Fiber may appear on the stack multiple times and for + // different reasons. We have to keep popping until the work-in-progress is + // no longer at the top of the stack. + + while (workInProgress === treeForkProvider) { + treeForkProvider = forkStack[--forkStackIndex]; + forkStack[forkStackIndex] = null; + treeForkCount = forkStack[--forkStackIndex]; + forkStack[forkStackIndex] = null; + } + + while (workInProgress === treeContextProvider) { + treeContextProvider = idStack[--idStackIndex]; + idStack[idStackIndex] = null; + treeContextOverflow = idStack[--idStackIndex]; + idStack[idStackIndex] = null; + treeContextId = idStack[--idStackIndex]; + idStack[idStackIndex] = null; + } +} + +export function getSuspendedTreeContext(): TreeContext | null { + warnIfNotHydrating(); + if (treeContextProvider !== null) { + return { + id: treeContextId, + overflow: treeContextOverflow, + }; + } else { + return null; + } +} + +export function restoreSuspendedTreeContext( + workInProgress: Fiber, + suspendedContext: TreeContext, +) { + warnIfNotHydrating(); + + idStack[idStackIndex++] = treeContextId; + idStack[idStackIndex++] = treeContextOverflow; + idStack[idStackIndex++] = treeContextProvider; + + treeContextId = suspendedContext.id; + treeContextOverflow = suspendedContext.overflow; + treeContextProvider = workInProgress; +} + +function warnIfNotHydrating() { + if (__DEV__) { + if (!getIsHydrating()) { + console.error( + 'Expected to be hydrating. This is a bug in React. Please file ' + + 'an issue.', + ); + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js index ba8bd4b573957..bb002b9e71b3a 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js @@ -50,8 +50,14 @@ import { popCachePool, } from './ReactFiberCacheComponent.new'; import {transferActualDuration} from './ReactProfilerTimer.new'; +import {popTreeContext} from './ReactFiberTreeContext.new'; function unwindWork(workInProgress: Fiber, renderLanes: Lanes) { + // Note: This intentionally doesn't check if we're hydrating because comparing + // to the current tree provider fiber is just as fast and less error-prone. + // Ideally we would have a special version of the work loop only + // for hydration. + popTreeContext(workInProgress); switch (workInProgress.tag) { case ClassComponent: { const Component = workInProgress.type; @@ -164,6 +170,11 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) { } function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) { + // Note: This intentionally doesn't check if we're hydrating because comparing + // to the current tree provider fiber is just as fast and less error-prone. + // Ideally we would have a special version of the work loop only + // for hydration. + popTreeContext(interruptedWork); switch (interruptedWork.tag) { case ClassComponent: { const childContextTypes = interruptedWork.type.childContextTypes; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js index ad8e479700db0..7f161513a4afa 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js @@ -50,8 +50,14 @@ import { popCachePool, } from './ReactFiberCacheComponent.old'; import {transferActualDuration} from './ReactProfilerTimer.old'; +import {popTreeContext} from './ReactFiberTreeContext.old'; function unwindWork(workInProgress: Fiber, renderLanes: Lanes) { + // Note: This intentionally doesn't check if we're hydrating because comparing + // to the current tree provider fiber is just as fast and less error-prone. + // Ideally we would have a special version of the work loop only + // for hydration. + popTreeContext(workInProgress); switch (workInProgress.tag) { case ClassComponent: { const Component = workInProgress.type; @@ -164,6 +170,11 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) { } function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) { + // Note: This intentionally doesn't check if we're hydrating because comparing + // to the current tree provider fiber is just as fast and less error-prone. + // Ideally we would have a special version of the work loop only + // for hydration. + popTreeContext(interruptedWork); switch (interruptedWork.tag) { case ClassComponent: { const childContextTypes = interruptedWork.type.childContextTypes; diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index e971828233c10..9dac4f40aad53 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -44,6 +44,7 @@ export type HookType = | 'useMutableSource' | 'useSyncExternalStore' | 'useOpaqueIdentifier' + | 'useId' | 'useCacheRefresh'; export type ContextDependency = { @@ -317,6 +318,7 @@ export type Dispatcher = {| getServerSnapshot?: () => T, ): T, useOpaqueIdentifier(): any, + useId(): string, useCacheRefresh?: () => (?() => T, ?T) => void, unstable_isNewReconciler?: boolean, diff --git a/packages/react-reconciler/src/clz32.js b/packages/react-reconciler/src/clz32.js new file mode 100644 index 0000000000000..80a9cfb911482 --- /dev/null +++ b/packages/react-reconciler/src/clz32.js @@ -0,0 +1,25 @@ +/** + * 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 + */ + +// TODO: This is pretty well supported by browsers. Maybe we can drop it. + +export const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback; + +// Count leading zeros. +// Based on: +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 +const log = Math.log; +const LN2 = Math.LN2; +function clz32Fallback(x: number): number { + const asUint = x >>> 0; + if (asUint === 0) { + return 32; + } + return (31 - ((log(asUint) / LN2) | 0)) | 0; +} diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 2596632652b43..bf1a2d56e0f93 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -17,8 +17,10 @@ import type { } from 'shared/ReactTypes'; import type {ResponseState, OpaqueIDType} from './ReactServerFormatConfig'; +import type {Task} from './ReactFizzServer'; import {readContext as readContextImpl} from './ReactFizzNewContext'; +import {getTreeId} from './ReactFizzTreeContext'; import {makeServerID} from './ReactServerFormatConfig'; @@ -45,12 +47,15 @@ type Hook = {| |}; let currentlyRenderingComponent: Object | null = null; +let currentlyRenderingTask: Task | null = null; let firstWorkInProgressHook: Hook | null = null; let workInProgressHook: Hook | null = null; // Whether the work-in-progress hook is a re-rendered hook let isReRender: boolean = false; // Whether an update was scheduled during the currently executing render pass. let didScheduleRenderPhaseUpdate: boolean = false; +// Counts the number of useId hooks in this component +let localIdCounter: number = 0; // Lazily created map of render-phase updates let renderPhaseUpdates: Map, Update> | null = null; // Counter to prevent infinite loops. @@ -163,18 +168,22 @@ function createWorkInProgressHook(): Hook { return workInProgressHook; } -export function prepareToUseHooks(componentIdentity: Object): void { +export function prepareToUseHooks(task: Task, componentIdentity: Object): void { currentlyRenderingComponent = componentIdentity; + currentlyRenderingTask = task; if (__DEV__) { isInHookUserCodeInDev = false; } // The following should have already been reset // didScheduleRenderPhaseUpdate = false; + // localIdCounter = 0; // firstWorkInProgressHook = null; // numberOfReRenders = 0; // renderPhaseUpdates = null; // workInProgressHook = null; + + localIdCounter = 0; } export function finishHooks( @@ -203,6 +212,14 @@ export function finishHooks( return children; } +export function checkDidRenderIdHook() { + // This should be called immediately after every finishHooks call. + // Conceptually, it's part of the return value of finishHooks; it's only a + // separate function to avoid using an array tuple. + const didRenderIdHook = localIdCounter !== 0; + return didRenderIdHook; +} + // Reset the internal hooks state if an error occurs while rendering a component export function resetHooksState(): void { if (__DEV__) { @@ -210,6 +227,7 @@ export function resetHooksState(): void { } currentlyRenderingComponent = null; + currentlyRenderingTask = null; didScheduleRenderPhaseUpdate = false; firstWorkInProgressHook = null; numberOfReRenders = 0; @@ -495,6 +513,24 @@ function useOpaqueIdentifier(): OpaqueIDType { return makeServerID(currentResponseState); } +function useId(): string { + const task: Task = (currentlyRenderingTask: any); + const treeId = getTreeId(task.treeContext); + + // Use a captial R prefix for server-generated ids. + let id = 'R:' + treeId; + + // Unless this is the first id at this level, append a number at the end + // that represents the position of this useId hook among all the useId + // hooks for this fiber. + const localId = localIdCounter++; + if (localId > 0) { + id += ':' + localId.toString(32); + } + + return id; +} + function unsupportedRefresh() { throw new Error('Cache cannot be refreshed during server rendering.'); } @@ -524,6 +560,7 @@ export const Dispatcher: DispatcherType = { useDeferredValue, useTransition, useOpaqueIdentifier, + useId, // Subscriptions are not setup in a server environment. useMutableSource, useSyncExternalStore, diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 6692b92648643..f06cbc8fb61a3 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -25,6 +25,7 @@ import type { } from './ReactServerFormatConfig'; import type {ContextSnapshot} from './ReactFizzNewContext'; import type {ComponentStackNode} from './ReactFizzComponentStack'; +import type {TreeContext} from './ReactFizzTreeContext'; import { scheduleWork, @@ -78,12 +79,14 @@ import { import { prepareToUseHooks, finishHooks, + checkDidRenderIdHook, resetHooksState, Dispatcher, currentResponseState, setCurrentResponseState, } from './ReactFizzHooks'; import {getStackByComponentStackNode} from './ReactFizzComponentStack'; +import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext'; import { getIteratorFn, @@ -134,7 +137,7 @@ type SuspenseBoundary = { fallbackAbortableTasks: Set, // used to cancel task on the fallback if the boundary completes or gets canceled. }; -type Task = { +export type Task = { node: ReactNodeList, ping: () => void, blockedBoundary: Root | SuspenseBoundary, @@ -142,6 +145,7 @@ type Task = { abortSet: Set, // the abortable set that this task belongs to legacyContext: LegacyContext, // the current legacy context that this task is executing in context: ContextSnapshot, // the current new context that this task is executing in + treeContext: TreeContext, // the current tree context that this task is executing in componentStack: null | ComponentStackNode, // DEV-only component stack }; @@ -265,6 +269,7 @@ export function createRequest( abortSet, emptyContextObject, rootContextSnapshot, + emptyTreeContext, ); pingedTasks.push(rootTask); return request; @@ -302,6 +307,7 @@ function createTask( abortSet: Set, legacyContext: LegacyContext, context: ContextSnapshot, + treeContext: TreeContext, ): Task { request.allPendingTasks++; if (blockedBoundary === null) { @@ -317,6 +323,7 @@ function createTask( abortSet, legacyContext, context, + treeContext, }: any); if (__DEV__) { task.componentStack = null; @@ -497,6 +504,7 @@ function renderSuspenseBoundary( fallbackAbortSet, task.legacyContext, task.context, + task.treeContext, ); if (__DEV__) { suspendedFallbackTask.componentStack = task.componentStack; @@ -564,7 +572,7 @@ function renderWithHooks( secondArg: SecondArg, ): any { const componentIdentity = {}; - prepareToUseHooks(componentIdentity); + prepareToUseHooks(task, componentIdentity); const result = Component(props, secondArg); return finishHooks(Component, props, result, secondArg); } @@ -671,6 +679,7 @@ function renderIndeterminateComponent( } const value = renderWithHooks(request, task, Component, props, legacyContext); + const hasId = checkDidRenderIdHook(); if (__DEV__) { // Support for module components is deprecated and is removed behind a flag. @@ -742,7 +751,21 @@ function renderIndeterminateComponent( } // We're now successfully past this task, and we don't have to pop back to // the previous task every again, so we can use the destructive recursive form. - renderNodeDestructive(request, task, value); + if (hasId) { + // This component materialized an id. We treat this as its own level, with + // a single "child" slot. + const prevTreeContext = task.treeContext; + const totalChildren = 1; + const index = 0; + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); + try { + renderNodeDestructive(request, task, value); + } finally { + task.treeContext = prevTreeContext; + } + } else { + renderNodeDestructive(request, task, value); + } } popComponentStackInDEV(task); } @@ -827,7 +850,22 @@ function renderForwardRef( ): void { pushFunctionComponentStackInDEV(task, type.render); const children = renderWithHooks(request, task, type.render, props, ref); - renderNodeDestructive(request, task, children); + const hasId = checkDidRenderIdHook(); + if (hasId) { + // This component materialized an id. We treat this as its own level, with + // a single "child" slot. + const prevTreeContext = task.treeContext; + const totalChildren = 1; + const index = 0; + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); + try { + renderNodeDestructive(request, task, children); + } finally { + task.treeContext = prevTreeContext; + } + } else { + renderNodeDestructive(request, task, children); + } popComponentStackInDEV(task); } @@ -1122,12 +1160,7 @@ function renderNodeDestructive( } if (isArray(node)) { - for (let i = 0; i < node.length; i++) { - // Recursively render the rest. We need to use the non-destructive form - // so that we can safely pop back up and render the sibling if something - // suspends. - renderNode(request, task, node[i]); - } + renderChildrenArray(request, task, node); return; } @@ -1138,18 +1171,23 @@ function renderNodeDestructive( } const iterator = iteratorFn.call(node); if (iterator) { + // We need to know how many total children are in this set, so that we + // can allocate enough id slots to acommodate them. So we must exhaust + // the iterator before we start recursively rendering the children. + // TODO: This is not great but I think it's inherent to the id + // generation algorithm. let step = iterator.next(); // If there are not entries, we need to push an empty so we start by checking that. if (!step.done) { + const children = []; do { - // Recursively render the rest. We need to use the non-destructive form - // so that we can safely pop back up and render the sibling if something - // suspends. - renderNode(request, task, step.value); + children.push(step.value); step = iterator.next(); } while (!step.done); + renderChildrenArray(request, task, children); return; } + return; } } @@ -1191,6 +1229,21 @@ function renderNodeDestructive( } } +function renderChildrenArray(request, task, children) { + const totalChildren = children.length; + for (let i = 0; i < totalChildren; i++) { + const prevTreeContext = task.treeContext; + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i); + try { + // We need to use the non-destructive form so that we can safely pop back + // up and render the sibling if something suspends. + renderNode(request, task, children[i]); + } finally { + task.treeContext = prevTreeContext; + } + } +} + function spawnNewSuspendedTask( request: Request, task: Task, @@ -1214,6 +1267,7 @@ function spawnNewSuspendedTask( task.abortSet, task.legacyContext, task.context, + task.treeContext, ); if (__DEV__) { if (task.componentStack !== null) { @@ -1257,6 +1311,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void { if (__DEV__) { task.componentStack = previousComponentStack; } + return; } else { // Restore the context. We assume that this will be restored by the inner // functions in case nothing throws so we don't use "finally" here. diff --git a/packages/react-server/src/ReactFizzTreeContext.js b/packages/react-server/src/ReactFizzTreeContext.js new file mode 100644 index 0000000000000..c9a47e5af72a6 --- /dev/null +++ b/packages/react-server/src/ReactFizzTreeContext.js @@ -0,0 +1,168 @@ +/** + * 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 + */ + +// Ids are base 32 strings whose binary representation corresponds to the +// position of a node in a tree. + +// Every time the tree forks into multiple children, we add additional bits to +// the left of the sequence that represent the position of the child within the +// current level of children. +// +// 00101 00010001011010101 +// ╰─┬─╯ ╰───────┬───────╯ +// Fork 5 of 20 Parent id +// +// The leading 0s are important. In the above example, you only need 3 bits to +// represent slot 5. However, you need 5 bits to represent all the forks at +// the current level, so we must account for the empty bits at the end. +// +// For this same reason, slots are 1-indexed instead of 0-indexed. Otherwise, +// the zeroth id at a level would be indistinguishable from its parent. +// +// If a node has only one child, and does not materialize an id (i.e. does not +// contain a useId hook), then we don't need to allocate any space in the +// sequence. It's treated as a transparent indirection. For example, these two +// trees produce the same ids: +// +// <> <> +// +// +// +// +// +// +// However, we cannot skip any node that materializes an id. Otherwise, a parent +// id that does not fork would be indistinguishable from its child id. For +// example, this tree does not fork, but the parent and child must have +// different ids. +// +// +// +// +// +// To handle this scenario, every time we materialize an id, we allocate a +// new level with a single slot. You can think of this as a fork with only one +// prong, or an array of children with length 1. +// +// It's possible for the the size of the sequence to exceed 32 bits, the max +// size for bitwise operations. When this happens, we make more room by +// converting the right part of the id to a string and storing it in an overflow +// variable. We use a base 32 string representation, because 32 is the largest +// power of 2 that is supported by toString(). We want the base to be large so +// that the resulting ids are compact, and we want the base to be a power of 2 +// because every log2(base) bits corresponds to a single character, i.e. every +// log2(32) = 5 bits. That means we can lop bits off the end 5 at a time without +// affecting the final result. + +export type TreeContext = { + +id: number, + +overflow: string, +}; + +export const emptyTreeContext = { + id: 1, + overflow: '', +}; + +export function getTreeId(context: TreeContext): string { + const overflow = context.overflow; + const idWithLeadingBit = context.id; + const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit); + return id.toString(32) + overflow; +} + +export function pushTreeContext( + baseContext: TreeContext, + totalChildren: number, + index: number, +): TreeContext { + const baseIdWithLeadingBit = baseContext.id; + const baseOverflow = baseContext.overflow; + + // The leftmost 1 marks the end of the sequence, non-inclusive. It's not part + // of the id; we use it to account for leading 0s. + const baseLength = getBitLength(baseIdWithLeadingBit) - 1; + const baseId = baseIdWithLeadingBit & ~(1 << baseLength); + + const slot = index + 1; + const length = getBitLength(totalChildren) + baseLength; + + // 30 is the max length we can store without overflowing, taking into + // consideration the leading 1 we use to mark the end of the sequence. + if (length > 30) { + // We overflowed the bitwise-safe range. Fall back to slower algorithm. + // This branch assumes the length of the base id is greater than 5; it won't + // work for smaller ids, because you need 5 bits per character. + // + // We encode the id in multiple steps: first the base id, then the + // remaining digits. + // + // Each 5 bit sequence corresponds to a single base 32 character. So for + // example, if the current id is 23 bits long, we can convert 20 of those + // bits into a string of 4 characters, with 3 bits left over. + // + // First calculate how many bits in the base id represent a complete + // sequence of characters. + const numberOfOverflowBits = baseLength - (baseLength % 5); + + // Then create a bitmask that selects only those bits. + const newOverflowBits = (1 << numberOfOverflowBits) - 1; + + // Select the bits, and convert them to a base 32 string. + const newOverflow = (baseId & newOverflowBits).toString(32); + + // Now we can remove those bits from the base id. + const restOfBaseId = baseId >> numberOfOverflowBits; + const restOfBaseLength = baseLength - numberOfOverflowBits; + + // Finally, encode the rest of the bits using the normal algorithm. Because + // we made more room, this time it won't overflow. + const restOfLength = getBitLength(totalChildren) + restOfBaseLength; + const restOfNewBits = slot << restOfBaseLength; + const id = restOfNewBits | restOfBaseId; + const overflow = newOverflow + baseOverflow; + return { + id: (1 << restOfLength) | id, + overflow, + }; + } else { + // Normal path + const newBits = slot << baseLength; + const id = newBits | baseId; + const overflow = baseOverflow; + return { + id: (1 << length) | id, + overflow, + }; + } +} + +function getBitLength(number: number): number { + return 32 - clz32(number); +} + +function getLeadingBit(id: number) { + return 1 << (getBitLength(id) - 1); +} + +// TODO: Math.clz32 is supported in Node 12+. Maybe we can drop the fallback. +const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback; + +// Count leading zeros. +// Based on: +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 +const log = Math.log; +const LN2 = Math.LN2; +function clz32Fallback(x: number): number { + const asUint = x >>> 0; + if (asUint === 0) { + return 32; + } + return (31 - ((log(asUint) / LN2) | 0)) | 0; +} diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 7bd7a95c25611..bba34065cc268 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -846,6 +846,7 @@ const Dispatcher: DispatcherType = { useImperativeHandle: (unsupportedHook: any), useEffect: (unsupportedHook: any), useOpaqueIdentifier: (unsupportedHook: any), + useId: (unsupportedHook: any), useMutableSource: (unsupportedHook: any), useSyncExternalStore: (unsupportedHook: any), useCacheRefresh(): (?() => T, ?T) => void { diff --git a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js index a3fe873729828..731124ea51593 100644 --- a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js +++ b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js @@ -43,6 +43,7 @@ export function waitForSuspense(fn: () => T): Promise { useDeferredValue: unsupported, useTransition: unsupported, useOpaqueIdentifier: unsupported, + useId: unsupported, useMutableSource: unsupported, useSyncExternalStore: unsupported, useCacheRefresh: unsupported, diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index c5854f8f6d398..a2e678c580b2e 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -41,6 +41,7 @@ export { unstable_getCacheForType, unstable_useCacheRefresh, unstable_useOpaqueIdentifier, + unstable_useId, useCallback, useContext, useDebugValue, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index 24fc9782595d4..20c22828583f5 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -37,6 +37,7 @@ export { unstable_getCacheForType, unstable_useCacheRefresh, unstable_useOpaqueIdentifier, + unstable_useId, useCallback, useContext, useDebugValue, diff --git a/packages/react/index.js b/packages/react/index.js index 9a6a99ee52189..3108c06c55284 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -62,6 +62,7 @@ export { unstable_getCacheForType, unstable_useCacheRefresh, unstable_useOpaqueIdentifier, + unstable_useId, useCallback, useContext, useDebugValue, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 8d08a43b90946..eef99fdabf2a4 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -40,6 +40,7 @@ export { unstable_getCacheForType, unstable_useCacheRefresh, unstable_useOpaqueIdentifier, + unstable_useId, useCallback, useContext, useDebugValue, diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index 867980fa5389d..517dc3f8fb2db 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -30,6 +30,7 @@ export { memo, startTransition, unstable_useOpaqueIdentifier, + unstable_useId, useCallback, useContext, useDebugValue, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index d29858c9b07fd..868538c83f59b 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -53,6 +53,7 @@ import { useTransition, useDeferredValue, useOpaqueIdentifier, + useId, useCacheRefresh, } from './ReactHooks'; import { @@ -127,5 +128,6 @@ export { // enableScopeAPI REACT_SCOPE_TYPE as unstable_Scope, useOpaqueIdentifier as unstable_useOpaqueIdentifier, + useId as unstable_useId, act, }; diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 1892f926c59cf..1f987de1671ba 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -174,6 +174,11 @@ export function useOpaqueIdentifier(): OpaqueIDType | void { return dispatcher.useOpaqueIdentifier(); } +export function useId(): string { + const dispatcher = resolveDispatcher(); + return dispatcher.useId(); +} + export function useMutableSource( source: MutableSource, getSnapshot: MutableSourceGetSnapshotFn, diff --git a/packages/react/unstable-shared-subset.experimental.js b/packages/react/unstable-shared-subset.experimental.js index a663ca8a5a89d..d556400750b4c 100644 --- a/packages/react/unstable-shared-subset.experimental.js +++ b/packages/react/unstable-shared-subset.experimental.js @@ -28,6 +28,7 @@ export { unstable_getCacheSignal, unstable_getCacheForType, unstable_useOpaqueIdentifier, + unstable_useId, useCallback, useContext, useDebugValue, From 8c4a05b8fb83624d5442f74138e7f263d666fd5d Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 1 Nov 2021 17:46:09 -0400 Subject: [PATCH 074/109] Remove @flow pragma comment from module registration start/stop templates (#22670) --- packages/shared/registerInternalModuleStart.js | 2 -- packages/shared/registerInternalModuleStop.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/shared/registerInternalModuleStart.js b/packages/shared/registerInternalModuleStart.js index aa1154fe9f1bf..1bab503c2a6da 100644 --- a/packages/shared/registerInternalModuleStart.js +++ b/packages/shared/registerInternalModuleStart.js @@ -3,8 +3,6 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow */ /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ diff --git a/packages/shared/registerInternalModuleStop.js b/packages/shared/registerInternalModuleStop.js index dab139f2557f9..44a69bed9ac37 100644 --- a/packages/shared/registerInternalModuleStop.js +++ b/packages/shared/registerInternalModuleStop.js @@ -3,8 +3,6 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow */ /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ From 75f3ddebfa0d9885ce8df42571cf0c09ad6c0a3b Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 1 Nov 2021 18:02:39 -0400 Subject: [PATCH 075/109] Remove experimental useOpaqueIdentifier API (#22672) useId is the updated version of this API. --- packages/react-art/src/ReactARTHostConfig.js | 18 - .../react-debug-tools/src/ReactDebugHooks.js | 26 - .../ReactHooksInspectionIntegration-test.js | 58 - .../src/backend/ReactSymbols.js | 3 - .../ReactDOMServerIntegrationHooks-test.js | 1109 ----------------- .../src/client/DOMPropertyOperations.js | 8 - .../react-dom/src/client/ReactDOMComponent.js | 10 - .../src/client/ReactDOMHostConfig.js | 45 - .../src/server/ReactDOMServerFormatConfig.js | 22 - .../ReactDOMServerLegacyFormatConfig.js | 6 - .../src/server/ReactPartialRenderer.js | 17 +- .../src/server/ReactPartialRendererHooks.js | 13 +- .../src/ReactFabricHostConfig.js | 20 - .../src/ReactNativeHostConfig.js | 19 - .../server/ReactNativeServerFormatConfig.js | 17 - .../src/ReactNoopServer.js | 6 - .../src/ReactFiberHooks.new.js | 139 --- .../src/ReactFiberHooks.old.js | 139 --- .../src/ReactFiberWorkLoop.new.js | 13 +- .../src/ReactFiberWorkLoop.old.js | 13 +- .../src/ReactInternalTypes.js | 2 - ...tIncrementalErrorHandling-test.internal.js | 16 +- .../src/forks/ReactFiberHostConfig.custom.js | 6 - packages/react-server/src/ReactFizzHooks.js | 9 +- .../react-server/src/ReactFlightServer.js | 1 - .../forks/ReactServerFormatConfig.custom.js | 2 - .../src/ReactSuspenseTestUtils.js | 1 - .../src/ReactTestHostConfig.js | 44 - packages/react/index.classic.fb.js | 1 - packages/react/index.experimental.js | 1 - packages/react/index.js | 1 - packages/react/index.modern.fb.js | 1 - packages/react/index.stable.js | 1 - packages/react/src/React.js | 2 - packages/react/src/ReactHooks.js | 6 - .../unstable-shared-subset.experimental.js | 1 - packages/shared/CheckStringCoercion.js | 12 - packages/shared/ReactSymbols.js | 2 - scripts/error-codes/codes.json | 2 +- 39 files changed, 22 insertions(+), 1790 deletions(-) diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index ec702bf9a70d4..47bced8a1274a 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -436,24 +436,6 @@ export function getInstanceFromNode(node) { throw new Error('Not implemented.'); } -export function isOpaqueHydratingObject(value: mixed): boolean { - throw new Error('Not implemented.'); -} - -export function makeOpaqueHydratingObject( - attemptToReadValue: () => void, -): OpaqueIDType { - throw new Error('Not implemented.'); -} - -export function makeClientId(): OpaqueIDType { - throw new Error('Not implemented.'); -} - -export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType { - throw new Error('Not implemented.'); -} - export function beforeActiveInstanceBlur(internalInstanceHandle: Object) { // noop } diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index eed8c46df7d6d..895e75359867c 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -18,13 +18,9 @@ import type { Fiber, Dispatcher as DispatcherType, } from 'react-reconciler/src/ReactInternalTypes'; -import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig'; - -import {NoMode} from 'react-reconciler/src/ReactTypeOfMode'; import ErrorStackParser from 'error-stack-parser'; import ReactSharedInternals from 'shared/ReactSharedInternals'; -import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols'; import { FunctionComponent, SimpleMemoComponent, @@ -53,8 +49,6 @@ type Dispatch = A => void; let primitiveStackCache: null | Map> = null; -let currentFiber: Fiber | null = null; - type Hook = { memoizedState: any, next: Hook | null, @@ -324,23 +318,6 @@ function useDeferredValue(value: T): T { return value; } -function useOpaqueIdentifier(): OpaqueIDType | void { - const hook = nextHook(); // State - if (currentFiber && currentFiber.mode === NoMode) { - nextHook(); // Effect - } - let value = hook === null ? undefined : hook.memoizedState; - if (value && value.$$typeof === REACT_OPAQUE_ID_TYPE) { - value = undefined; - } - hookLog.push({ - primitive: 'OpaqueIdentifier', - stackError: new Error(), - value, - }); - return value; -} - function useId(): string { const hook = nextHook(); const id = hook !== null ? hook.memoizedState : ''; @@ -371,7 +348,6 @@ const Dispatcher: DispatcherType = { useMutableSource, useSyncExternalStore, useDeferredValue, - useOpaqueIdentifier, useId, }; @@ -767,8 +743,6 @@ export function inspectHooksOfFiber( currentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; } - currentFiber = fiber; - if ( fiber.tag !== FunctionComponent && fiber.tag !== SimpleMemoComponent && diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index d17a01a258277..559a0bf75e52f 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -598,64 +598,6 @@ describe('ReactHooksInspectionIntegration', () => { ]); }); - it('should support composite useOpaqueIdentifier hook', () => { - function Foo(props) { - const id = React.unstable_useOpaqueIdentifier(); - const [state] = React.useState(() => 'hello', []); - return
{state}
; - } - - const renderer = ReactTestRenderer.create(); - const childFiber = renderer.root.findByType(Foo)._currentFiber(); - const tree = ReactDebugTools.inspectHooksOfFiber(childFiber); - - expect(tree.length).toEqual(2); - - expect(tree[0].id).toEqual(0); - expect(tree[0].isStateEditable).toEqual(false); - expect(tree[0].name).toEqual('OpaqueIdentifier'); - expect(String(tree[0].value).startsWith('c_')).toBe(true); - - expect(tree[1]).toEqual({ - id: 1, - isStateEditable: true, - name: 'State', - value: 'hello', - subHooks: [], - }); - }); - - it('should support composite useOpaqueIdentifier hook in concurrent mode', () => { - function Foo(props) { - const id = React.unstable_useOpaqueIdentifier(); - const [state] = React.useState('hello'); - return
{state}
; - } - - const renderer = ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); - expect(Scheduler).toFlushWithoutYielding(); - - const childFiber = renderer.root.findByType(Foo)._currentFiber(); - const tree = ReactDebugTools.inspectHooksOfFiber(childFiber); - - expect(tree.length).toEqual(2); - - expect(tree[0].id).toEqual(0); - expect(tree[0].isStateEditable).toEqual(false); - expect(tree[0].name).toEqual('OpaqueIdentifier'); - expect(String(tree[0].value).startsWith('c_')).toBe(true); - - expect(tree[1]).toEqual({ - id: 1, - isStateEditable: true, - name: 'State', - value: 'hello', - subHooks: [], - }); - }); - it('should support useId hook', () => { function Foo(props) { const id = React.unstable_useId(); diff --git a/packages/react-devtools-shared/src/backend/ReactSymbols.js b/packages/react-devtools-shared/src/backend/ReactSymbols.js index c6515f02edc51..ebc6920be8d2f 100644 --- a/packages/react-devtools-shared/src/backend/ReactSymbols.js +++ b/packages/react-devtools-shared/src/backend/ReactSymbols.js @@ -40,9 +40,6 @@ export const LAZY_SYMBOL_STRING = 'Symbol(react.lazy)'; export const MEMO_NUMBER = 0xead3; export const MEMO_SYMBOL_STRING = 'Symbol(react.memo)'; -export const OPAQUE_ID_NUMBER = 0xeae0; -export const OPAQUE_ID_SYMBOL_STRING = 'Symbol(react.opaque.id)'; - export const PORTAL_NUMBER = 0xeaca; export const PORTAL_SYMBOL_STRING = 'Symbol(react.portal)'; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js index ba2b22437bc1b..114c522313a79 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js @@ -17,8 +17,6 @@ let React; let ReactDOM; let ReactDOMServer; let ReactTestUtils; -let act; -let Scheduler; let useState; let useReducer; let useEffect; @@ -30,7 +28,6 @@ let useImperativeHandle; let useInsertionEffect; let useLayoutEffect; let useDebugValue; -let useOpaqueIdentifier; let forwardRef; let yieldedValues; let yieldValue; @@ -44,8 +41,6 @@ function initModules() { ReactDOM = require('react-dom'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); - Scheduler = require('scheduler'); - act = require('jest-react').act; useState = React.useState; useReducer = React.useReducer; useEffect = React.useEffect; @@ -57,7 +52,6 @@ function initModules() { useImperativeHandle = React.useImperativeHandle; useInsertionEffect = React.useInsertionEffect; useLayoutEffect = React.useLayoutEffect; - useOpaqueIdentifier = React.unstable_useOpaqueIdentifier; forwardRef = React.forwardRef; yieldedValues = []; @@ -83,9 +77,6 @@ const { itRenders, itThrowsWhenRendering, serverRender, - streamRender, - clientCleanRender, - clientRenderOnServerString, } = ReactDOMServerIntegrationUtils(initModules); describe('ReactDOMServerHooks', () => { @@ -910,1104 +901,4 @@ describe('ReactDOMServerHooks', () => { ); expect(container.children[0].textContent).toEqual('0'); }); - - describe('useOpaqueIdentifier', () => { - it('generates unique ids for server string render', async () => { - function App(props) { - const idOne = useOpaqueIdentifier(); - const idTwo = useOpaqueIdentifier(); - return ( -
-
-
- - -
- ); - } - - const domNode = await serverRender(); - expect(domNode.children.length).toEqual(4); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - domNode.children[1].getAttribute('id'), - ); - expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( - domNode.children[3].getAttribute('id'), - ); - expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( - domNode.children[2].getAttribute('aria-labelledby'), - ); - expect( - domNode.children[0].getAttribute('aria-labelledby'), - ).not.toBeNull(); - expect( - domNode.children[2].getAttribute('aria-labelledby'), - ).not.toBeNull(); - }); - - it('generates unique ids for server stream render', async () => { - function App(props) { - const idOne = useOpaqueIdentifier(); - const idTwo = useOpaqueIdentifier(); - return ( -
-
-
- - -
- ); - } - - const domNode = await streamRender(); - expect(domNode.children.length).toEqual(4); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - domNode.children[1].getAttribute('id'), - ); - expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( - domNode.children[3].getAttribute('id'), - ); - expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( - domNode.children[2].getAttribute('aria-labelledby'), - ); - expect( - domNode.children[0].getAttribute('aria-labelledby'), - ).not.toBeNull(); - expect( - domNode.children[2].getAttribute('aria-labelledby'), - ).not.toBeNull(); - }); - - it('generates unique ids for client render', async () => { - function App(props) { - const idOne = useOpaqueIdentifier(); - const idTwo = useOpaqueIdentifier(); - return ( -
-
-
- - -
- ); - } - - const domNode = await clientCleanRender(); - expect(domNode.children.length).toEqual(4); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - domNode.children[1].getAttribute('id'), - ); - expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( - domNode.children[3].getAttribute('id'), - ); - expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( - domNode.children[2].getAttribute('aria-labelledby'), - ); - expect( - domNode.children[0].getAttribute('aria-labelledby'), - ).not.toBeNull(); - expect( - domNode.children[2].getAttribute('aria-labelledby'), - ).not.toBeNull(); - }); - - it('generates unique ids for client render on good server markup', async () => { - function App(props) { - const idOne = useOpaqueIdentifier(); - const idTwo = useOpaqueIdentifier(); - return ( -
-
-
- - -
- ); - } - - const domNode = await clientRenderOnServerString(); - expect(domNode.children.length).toEqual(4); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - domNode.children[1].getAttribute('id'), - ); - expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( - domNode.children[3].getAttribute('id'), - ); - expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( - domNode.children[2].getAttribute('aria-labelledby'), - ); - expect( - domNode.children[0].getAttribute('aria-labelledby'), - ).not.toBeNull(); - expect( - domNode.children[2].getAttribute('aria-labelledby'), - ).not.toBeNull(); - }); - - it('useOpaqueIdentifier does not change id even if the component updates during client render', async () => { - let _setShowId; - function App() { - const id = useOpaqueIdentifier(); - const [showId, setShowId] = useState(false); - _setShowId = setShowId; - return ( -
-
- {showId &&
} -
- ); - } - - const domNode = await clientCleanRender(); - const oldClientId = domNode.children[0].getAttribute('aria-labelledby'); - - expect(domNode.children.length).toEqual(1); - expect(oldClientId).not.toBeNull(); - - await act(async () => _setShowId(true)); - - expect(domNode.children.length).toEqual(2); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - domNode.children[1].getAttribute('id'), - ); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - oldClientId, - ); - }); - - it('useOpaqueIdentifier identifierPrefix works for server renderer and does not clash', async () => { - function ChildTwo({id}) { - return
Child Three
; - } - function App() { - const id = useOpaqueIdentifier(); - const idTwo = useOpaqueIdentifier(); - - return ( -
-
Child One
- -
Child Three
-
Child Four
-
- ); - } - - const containerOne = document.createElement('div'); - document.body.append(containerOne); - - containerOne.innerHTML = ReactDOMServer.renderToString(, { - identifierPrefix: 'one', - }); - - const containerTwo = document.createElement('div'); - document.body.append(containerTwo); - - containerTwo.innerHTML = ReactDOMServer.renderToString(, { - identifierPrefix: 'two', - }); - - expect(document.body.children.length).toEqual(2); - const childOne = document.body.children[0]; - const childTwo = document.body.children[1]; - - expect( - childOne.children[0].children[0].getAttribute('aria-labelledby'), - ).toEqual(childOne.children[0].children[1].getAttribute('id')); - expect( - childOne.children[0].children[2].getAttribute('aria-labelledby'), - ).toEqual(childOne.children[0].children[3].getAttribute('id')); - - expect( - childOne.children[0].children[0].getAttribute('aria-labelledby'), - ).not.toEqual( - childOne.children[0].children[2].getAttribute('aria-labelledby'), - ); - - expect( - childOne.children[0].children[0] - .getAttribute('aria-labelledby') - .startsWith('one'), - ).toBe(true); - expect( - childOne.children[0].children[2] - .getAttribute('aria-labelledby') - .includes('one'), - ).toBe(true); - - expect( - childTwo.children[0].children[0].getAttribute('aria-labelledby'), - ).toEqual(childTwo.children[0].children[1].getAttribute('id')); - expect( - childTwo.children[0].children[2].getAttribute('aria-labelledby'), - ).toEqual(childTwo.children[0].children[3].getAttribute('id')); - - expect( - childTwo.children[0].children[0].getAttribute('aria-labelledby'), - ).not.toEqual( - childTwo.children[0].children[2].getAttribute('aria-labelledby'), - ); - - expect( - childTwo.children[0].children[0] - .getAttribute('aria-labelledby') - .startsWith('two'), - ).toBe(true); - expect( - childTwo.children[0].children[2] - .getAttribute('aria-labelledby') - .startsWith('two'), - ).toBe(true); - }); - - it('useOpaqueIdentifier identifierPrefix works for multiple reads on a streaming server renderer', async () => { - function ChildTwo() { - const id = useOpaqueIdentifier(); - - return
Child Two
; - } - - function App() { - const id = useOpaqueIdentifier(); - - return ( - <> -
Child One
- -
Aria One
- - ); - } - - const container = document.createElement('div'); - document.body.append(container); - - const streamOne = ReactDOMServer.renderToNodeStream(, { - identifierPrefix: 'one', - }).setEncoding('utf8'); - const streamTwo = ReactDOMServer.renderToNodeStream(, { - identifierPrefix: 'two', - }).setEncoding('utf8'); - - const streamOneIsDone = new Promise((resolve, reject) => { - streamOne.on('end', () => resolve()); - streamOne.on('error', e => reject(e)); - }); - const streamTwoIsDone = new Promise((resolve, reject) => { - streamTwo.on('end', () => resolve()); - streamTwo.on('error', e => reject(e)); - }); - - const containerOne = document.createElement('div'); - const containerTwo = document.createElement('div'); - - streamOne._read(10); - streamTwo._read(10); - - containerOne.innerHTML = streamOne.read(); - containerTwo.innerHTML = streamTwo.read(); - - expect(containerOne.children[0].getAttribute('id')).not.toEqual( - containerOne.children[1].getAttribute('id'), - ); - expect(containerTwo.children[0].getAttribute('id')).not.toEqual( - containerTwo.children[1].getAttribute('id'), - ); - expect(containerOne.children[0].getAttribute('id')).not.toEqual( - containerTwo.children[0].getAttribute('id'), - ); - expect(containerOne.children[0].getAttribute('id').includes('one')).toBe( - true, - ); - expect(containerOne.children[1].getAttribute('id').includes('one')).toBe( - true, - ); - expect(containerTwo.children[0].getAttribute('id').includes('two')).toBe( - true, - ); - expect(containerTwo.children[1].getAttribute('id').includes('two')).toBe( - true, - ); - - expect(containerOne.children[1].getAttribute('id')).not.toEqual( - containerTwo.children[1].getAttribute('id'), - ); - expect(containerOne.children[0].getAttribute('id')).toEqual( - containerOne.children[2].getAttribute('aria-labelledby'), - ); - expect(containerTwo.children[0].getAttribute('id')).toEqual( - containerTwo.children[2].getAttribute('aria-labelledby'), - ); - - // Exhaust the rest of the stream - class Sink extends require('stream').Writable { - _write(chunk, encoding, done) { - done(); - } - } - streamOne.pipe(new Sink()); - streamTwo.pipe(new Sink()); - - await Promise.all([streamOneIsDone, streamTwoIsDone]); - }); - - it('useOpaqueIdentifier: IDs match when, after hydration, a new component that uses the ID is rendered', async () => { - let _setShowDiv; - function App() { - const id = useOpaqueIdentifier(); - const [showDiv, setShowDiv] = useState(false); - _setShowDiv = setShowDiv; - - return ( -
-
Child One
- {showDiv &&
Child Two
} -
- ); - } - - const container = document.createElement('div'); - document.body.append(container); - - container.innerHTML = ReactDOMServer.renderToString(); - const root = ReactDOM.createRoot(container, {hydrate: true}); - root.render(); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - expect(container.children[0].children.length).toEqual(1); - const oldServerId = container.children[0].children[0].getAttribute('id'); - expect(oldServerId).not.toBeNull(); - - await act(async () => { - _setShowDiv(true); - }); - expect(container.children[0].children.length).toEqual(2); - expect(container.children[0].children[0].getAttribute('id')).toEqual( - container.children[0].children[1].getAttribute('id'), - ); - expect(container.children[0].children[0].getAttribute('id')).not.toEqual( - oldServerId, - ); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toBeNull(); - }); - - it('useOpaqueIdentifier: IDs match when, after hydration, a new component that uses the ID is rendered for legacy', async () => { - let _setShowDiv; - function App() { - const id = useOpaqueIdentifier(); - const [showDiv, setShowDiv] = useState(false); - _setShowDiv = setShowDiv; - - return ( -
-
Child One
- {showDiv &&
Child Two
} -
- ); - } - - const container = document.createElement('div'); - document.body.append(container); - - container.innerHTML = ReactDOMServer.renderToString(); - ReactDOM.hydrate(, container); - - expect(container.children[0].children.length).toEqual(1); - const oldServerId = container.children[0].children[0].getAttribute('id'); - expect(oldServerId).not.toBeNull(); - - await act(async () => { - _setShowDiv(true); - }); - expect(container.children[0].children.length).toEqual(2); - expect(container.children[0].children[0].getAttribute('id')).toEqual( - container.children[0].children[1].getAttribute('id'), - ); - expect(container.children[0].children[0].getAttribute('id')).not.toEqual( - oldServerId, - ); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toBeNull(); - }); - - it('useOpaqueIdentifier: ID is not used during hydration but is used in an update', async () => { - let _setShow; - function App({unused}) { - Scheduler.unstable_yieldValue('App'); - const id = useOpaqueIdentifier(); - const [show, setShow] = useState(false); - _setShow = setShow; - return ( -
- {'Child One'} -
- ); - } - - const container = document.createElement('div'); - document.body.append(container); - container.innerHTML = ReactDOMServer.renderToString(); - const root = ReactDOM.createRoot(container, {hydrate: true}); - act(() => { - root.render(); - }); - expect(Scheduler).toHaveYielded(['App', 'App']); - // The ID goes from not being used to being added to the page - act(() => { - _setShow(true); - }); - expect(Scheduler).toHaveYielded(['App', 'App']); - expect( - container.getElementsByTagName('span')[0].getAttribute('id'), - ).not.toBeNull(); - }); - - it('useOpaqueIdentifier: ID is not used during hydration but is used in an update in legacy', async () => { - let _setShow; - function App({unused}) { - Scheduler.unstable_yieldValue('App'); - const id = useOpaqueIdentifier(); - const [show, setShow] = useState(false); - _setShow = setShow; - return ( -
- {'Child One'} -
- ); - } - - const container = document.createElement('div'); - document.body.append(container); - container.innerHTML = ReactDOMServer.renderToString(); - ReactDOM.hydrate(, container); - expect(Scheduler).toHaveYielded(['App', 'App']); - // The ID goes from not being used to being added to the page - act(() => { - _setShow(true); - }); - expect(Scheduler).toHaveYielded(['App']); - expect( - container.getElementsByTagName('span')[0].getAttribute('id'), - ).not.toBeNull(); - }); - - it('useOpaqueIdentifier: flushSync', async () => { - let _setShow; - function App() { - const id = useOpaqueIdentifier(); - const [show, setShow] = useState(false); - _setShow = setShow; - return ( -
- {'Child One'} -
- ); - } - - const container = document.createElement('div'); - document.body.append(container); - container.innerHTML = ReactDOMServer.renderToString(); - const root = ReactDOM.createRoot(container, {hydrate: true}); - act(() => { - root.render(); - }); - - // The ID goes from not being used to being added to the page - act(() => { - ReactDOM.flushSync(() => { - _setShow(true); - }); - }); - expect( - container.getElementsByTagName('span')[0].getAttribute('id'), - ).not.toBeNull(); - }); - - it('useOpaqueIdentifier: children with id hydrates before other children if ID updates', async () => { - let _setShow; - - const child1Ref = React.createRef(); - const childWithIDRef = React.createRef(); - const setShowRef = React.createRef(); - - // RENAME THESE - function Child1() { - Scheduler.unstable_yieldValue('Child One'); - return {'Child One'}; - } - - function Child2() { - Scheduler.unstable_yieldValue('Child Two'); - return {'Child Two'}; - } - - const Children = React.memo(function Children() { - return ( - - - - - ); - }); - - function ChildWithID({parentID}) { - Scheduler.unstable_yieldValue('Child with ID'); - return ( - - {'Child with ID'} - - ); - } - - const ChildrenWithID = React.memo(function ChildrenWithID({parentID}) { - return ( - - - - ); - }); - - function App() { - const id = useOpaqueIdentifier(); - const [show, setShow] = useState(false); - _setShow = setShow; - return ( -
- - - {show && ( - - {'Child Three'} - - )} -
- ); - } - - const container = document.createElement('div'); - container.innerHTML = ReactDOMServer.renderToString(); - expect(Scheduler).toHaveYielded([ - 'Child One', - 'Child Two', - 'Child with ID', - ]); - expect(container.textContent).toEqual('Child OneChild TwoChild with ID'); - - const serverId = container - .getElementsByTagName('span')[2] - .getAttribute('id'); - expect(serverId).not.toBeNull(); - - const root = ReactDOM.createRoot(container, {hydrate: true}); - root.render(); - expect(Scheduler).toHaveYielded([]); - - //Hydrate just child one before updating state - expect(Scheduler).toFlushAndYieldThrough(['Child One']); - expect(child1Ref.current).toBe(null); - expect(Scheduler).toHaveYielded([]); - - act(() => { - _setShow(true); - - // State update should trigger the ID to update, which changes the props - // of ChildWithID. This should cause ChildWithID to hydrate before Children - - expect(Scheduler).toFlushAndYieldThrough([ - 'Child with ID', - // Fallbacks are immediately committed in TestUtils version - // of act - // 'Child with ID', - // 'Child with ID', - 'Child One', - 'Child Two', - ]); - - expect(child1Ref.current).toBe(null); - expect(childWithIDRef.current).toEqual( - container.getElementsByTagName('span')[2], - ); - - expect(setShowRef.current).toEqual( - container.getElementsByTagName('span')[3], - ); - - expect(childWithIDRef.current.getAttribute('id')).toEqual( - setShowRef.current.getAttribute('aria-labelledby'), - ); - expect(childWithIDRef.current.getAttribute('id')).not.toEqual(serverId); - }); - - // Children hydrates after ChildWithID - expect(child1Ref.current).toBe(container.getElementsByTagName('span')[0]); - - Scheduler.unstable_flushAll(); - - expect(Scheduler).toHaveYielded([]); - }); - - it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => { - let suspend = true; - let resolve; - const promise = new Promise(resolvePromise => (resolve = resolvePromise)); - - function Child({text}) { - if (suspend) { - throw promise; - } else { - return text; - } - } - - function RenderedChild() { - useEffect(() => { - Scheduler.unstable_yieldValue('Child did commit'); - }); - return null; - } - - function App() { - const id = useOpaqueIdentifier(); - useEffect(() => { - Scheduler.unstable_yieldValue('Did commit'); - }); - return ( -
-
Child One
- - -
- -
-
-
- ); - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - container.innerHTML = ReactDOMServer.renderToString(); - - suspend = true; - const root = ReactDOM.createRoot(container, {hydrate: true}); - await act(async () => { - root.render(); - }); - jest.runAllTimers(); - expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']); - expect(Scheduler).toFlushAndYield([]); - - const serverId = container.children[0].children[0].getAttribute('id'); - expect(container.children[0].children.length).toEqual(1); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toBeNull(); - - await act(async () => { - suspend = false; - resolve(); - await promise; - }); - - expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']); - expect(Scheduler).toFlushAndYield([]); - jest.runAllTimers(); - - expect(container.children[0].children.length).toEqual(2); - expect(container.children[0].children[0].getAttribute('id')).toEqual( - container.children[0].children[1].getAttribute('id'), - ); - expect(container.children[0].children[0].getAttribute('id')).not.toEqual( - serverId, - ); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toBeNull(); - }); - - it('useOpaqueIdentifier warn when there is a hydration error', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ; - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - // This is the wrong HTML string - container.innerHTML = ''; - ReactDOM.createRoot(container, {hydrate: true}).render(); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( - [ - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in
.', - 'Warning: Expected server HTML to contain a matching
in
.', - ], - {withoutStack: 1}, - ); - }); - - it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => { - let suspend = true; - - function Child({text}) { - if (suspend) { - throw new Promise(() => {}); - } else { - return text; - } - } - - function RenderedChild() { - useEffect(() => { - Scheduler.unstable_yieldValue('Child did commit'); - }); - return null; - } - - function App() { - const id = useOpaqueIdentifier(); - useEffect(() => { - Scheduler.unstable_yieldValue('Did commit'); - }); - return ( -
-
Child One
- - -
- -
-
-
- ); - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - container.innerHTML = ReactDOMServer.renderToString(); - - suspend = false; - const root = ReactDOM.createRoot(container, {hydrate: true}); - await act(async () => { - root.render(); - }); - jest.runAllTimers(); - expect(Scheduler).toHaveYielded([ - 'Child did commit', - 'Did commit', - 'Child did commit', - 'Did commit', - ]); - expect(Scheduler).toFlushAndYield([]); - - expect(container.children[0].children.length).toEqual(2); - expect(container.children[0].children[0].getAttribute('id')).toEqual( - container.children[0].children[1].getAttribute('id'), - ); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toBeNull(); - }); - - it('useOpaqueIdentifier warn when there is a hydration error', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ; - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - // This is the wrong HTML string - container.innerHTML = ''; - ReactDOM.createRoot(container, {hydrate: true}).render(); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( - [ - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in
.', - 'Warning: Expected server HTML to contain a matching
in
.', - ], - {withoutStack: 1}, - ); - }); - - it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ; - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - // This is the wrong HTML string - container.innerHTML = ''; - ReactDOM.createRoot(container, {hydrate: true}).render(); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( - [ - 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: 1}, - ); - }); - - it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ; - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - // This is the wrong HTML string - container.innerHTML = ''; - ReactDOM.createRoot(container, {hydrate: true}).render(); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( - [ - 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: 1}, - ); - }); - - it('useOpaqueIdentifier warns if you try to use the result as a string in a child component', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ; - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - container.innerHTML = ReactDOMServer.renderToString(); - ReactDOM.createRoot(container, {hydrate: true}).render(); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( - [ - 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: 1}, - ); - }); - - it('useOpaqueIdentifier warns if you try to use the result as a string', async () => { - function App() { - const id = useOpaqueIdentifier(); - return
; - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - container.innerHTML = ReactDOMServer.renderToString(); - ReactDOM.createRoot(container, {hydrate: true}).render(); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( - [ - 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: 1}, - ); - }); - - it('useOpaqueIdentifier warns if you try to use the result as a string in a child component wrapped in a Suspense', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ( - - - - ); - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - container.innerHTML = ReactDOMServer.renderToString(); - - ReactDOM.createRoot(container, {hydrate: true}).render(); - - if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) { - expect(() => Scheduler.unstable_flushAll()).toErrorDev([ - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ]); - } else { - // This error isn't surfaced to the user; only the warning is. - // The error is just the mechanism that restarts the render. - expect(() => - expect(() => Scheduler.unstable_flushAll()).toThrow( - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ), - ).toErrorDev([ - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ]); - } - }); - - it('useOpaqueIdentifier warns if you try to add the result as a number in a child component wrapped in a Suspense', async () => { - function Child({appId}) { - return
; - } - function App() { - const [show] = useState(false); - const id = useOpaqueIdentifier(); - return ( - - {show &&
} - - - ); - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - container.innerHTML = ReactDOMServer.renderToString(); - - ReactDOM.createRoot(container, {hydrate: true}).render(); - - if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) { - expect(() => Scheduler.unstable_flushAll()).toErrorDev([ - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ]); - } else { - // This error isn't surfaced to the user; only the warning is. - // The error is just the mechanism that restarts the render. - expect(() => - expect(() => Scheduler.unstable_flushAll()).toThrow( - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ), - ).toErrorDev([ - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ]); - } - }); - - it('useOpaqueIdentifier with two opaque identifiers on the same page', () => { - let _setShow; - - function App() { - const id1 = useOpaqueIdentifier(); - const id2 = useOpaqueIdentifier(); - const [show, setShow] = useState(true); - _setShow = setShow; - - return ( -
- - {show ? ( - {'Child'} - ) : ( - {'Child'} - )} - - {'test'} -
- ); - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - container.innerHTML = ReactDOMServer.renderToString(); - - const serverID = container - .getElementsByTagName('span')[0] - .getAttribute('id'); - expect(serverID).not.toBeNull(); - expect( - container - .getElementsByTagName('span')[1] - .getAttribute('aria-labelledby'), - ).toEqual(serverID); - - ReactDOM.createRoot(container, {hydrate: true}).render(); - jest.runAllTimers(); - expect(Scheduler).toHaveYielded([]); - expect(Scheduler).toFlushAndYield([]); - - act(() => { - _setShow(false); - }); - - expect( - container - .getElementsByTagName('span')[1] - .getAttribute('aria-labelledby'), - ).toEqual(serverID); - expect( - container.getElementsByTagName('span')[0].getAttribute('id'), - ).not.toEqual(serverID); - expect( - container.getElementsByTagName('span')[0].getAttribute('id'), - ).not.toBeNull(); - }); - - it('useOpaqueIdentifier with multiple ids in nested components', async () => { - function DivWithId({id, children}) { - return
{children}
; - } - - let setShowMore; - function App() { - const outerId = useOpaqueIdentifier(); - const innerId = useOpaqueIdentifier(); - const [showMore, _setShowMore] = useState(false); - setShowMore = _setShowMore; - return showMore ? ( - - - - ) : null; - } - - const container = document.createElement('div'); - container.innerHTML = ReactDOMServer.renderToString(); - - await act(async () => { - ReactDOM.hydrateRoot(container, ); - }); - - // Show additional content that wasn't part of the initial server- - // rendered repsonse. - await act(async () => { - setShowMore(true); - }); - const [div1, div2] = container.getElementsByTagName('div'); - expect(typeof div1.getAttribute('id')).toBe('string'); - expect(typeof div2.getAttribute('id')).toBe('string'); - }); - }); }); diff --git a/packages/react-dom/src/client/DOMPropertyOperations.js b/packages/react-dom/src/client/DOMPropertyOperations.js index 30fed05aed609..ad94f7ccb7026 100644 --- a/packages/react-dom/src/client/DOMPropertyOperations.js +++ b/packages/react-dom/src/client/DOMPropertyOperations.js @@ -21,7 +21,6 @@ import { enableTrustedTypesIntegration, } from 'shared/ReactFeatureFlags'; import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; -import {isOpaqueHydratingObject} from './ReactDOMHostConfig'; import type {PropertyInfo} from '../shared/DOMProperty'; @@ -119,13 +118,6 @@ export function getValueForAttribute( if (!isAttributeNameSafe(name)) { return; } - - // If the object is an opaque reference ID, it's expected that - // the next prop is different than the server value, so just return - // expected - if (isOpaqueHydratingObject(expected)) { - return expected; - } if (!node.hasAttribute(name)) { return expected === undefined ? undefined : null; } diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index 2d347639eba45..aa9b0281e2f8d 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -68,7 +68,6 @@ import possibleStandardNames from '../shared/possibleStandardNames'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; -import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols'; import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags'; import { @@ -775,15 +774,6 @@ export function diffProperties( // to update this element. updatePayload = []; } - } else if ( - typeof nextProp === 'object' && - nextProp !== null && - nextProp.$$typeof === REACT_OPAQUE_ID_TYPE - ) { - // If we encounter useOpaqueReference's opaque object, this means we are hydrating. - // In this case, call the opaque object's toString function which generates a new client - // ID so client and server IDs match and throws to rerender. - nextProp.toString(); } else { // For any other property we always add it to the queue and then we // filter it out using the allowed property list during the commit. diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 8a5417ae49272..64fd789b7a1e8 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -59,7 +59,6 @@ import { } from '../shared/HTMLNodeType'; import dangerousStyleValue from '../shared/dangerousStyleValue'; -import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols'; import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying'; import { @@ -124,13 +123,6 @@ export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; export type RendererInspectionConfig = $ReadOnly<{||}>; -export opaque type OpaqueIDType = - | string - | { - toString: () => string | void, - valueOf: () => string | void, - }; - type SelectionInformation = {| focusedElem: null | HTMLElement, selectionRange: mixed, @@ -1089,43 +1081,6 @@ export function getInstanceFromNode(node: HTMLElement): null | Object { return getClosestInstanceFromNode(node) || null; } -let clientId: number = 0; -export function makeClientId(): OpaqueIDType { - return 'r:' + (clientId++).toString(36); -} - -export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType { - const id = 'r:' + (clientId++).toString(36); - return { - toString() { - warnOnAccessInDEV(); - return id; - }, - valueOf() { - warnOnAccessInDEV(); - return id; - }, - }; -} - -export function isOpaqueHydratingObject(value: mixed): boolean { - return ( - value !== null && - typeof value === 'object' && - value.$$typeof === REACT_OPAQUE_ID_TYPE - ); -} - -export function makeOpaqueHydratingObject( - attemptToReadValue: () => void, -): OpaqueIDType { - return { - $$typeof: REACT_OPAQUE_ID_TYPE, - toString: attemptToReadValue, - valueOf: attemptToReadValue, - }; -} - export function preparePortalMount(portalInstance: Instance): void { listenToAllSupportedEvents(portalInstance); } diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index 2012392004e52..65a10974b0196 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -64,9 +64,7 @@ export type ResponseState = { placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, - opaqueIdentifierPrefix: string, nextSuspenseID: number, - nextOpaqueID: number, sentCompleteSegmentFunction: boolean, sentCompleteBoundaryFunction: boolean, sentClientRenderFunction: boolean, // We allow the legacy renderer to extend this object. @@ -127,9 +125,7 @@ export function createResponseState( placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'), segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'), boundaryPrefix: idPrefix + 'B:', - opaqueIdentifierPrefix: idPrefix + 'R:', nextSuspenseID: 0, - nextOpaqueID: 0, sentCompleteSegmentFunction: false, sentCompleteBoundaryFunction: false, sentClientRenderFunction: false, @@ -233,24 +229,6 @@ export function assignSuspenseBoundaryID( ); } -export type OpaqueIDType = string; - -export function makeServerID( - responseState: null | ResponseState, -): OpaqueIDType { - if (responseState === null) { - throw new Error( - 'Invalid hook call. Hooks can only be called inside of the body of a function component.', - ); - } - - // TODO: This is not deterministic since it's created during render. - return ( - responseState.opaqueIdentifierPrefix + - (responseState.nextOpaqueID++).toString(36) - ); -} - function encodeHTMLTextNode(text: string): string { return escapeTextForBrowser(text); } diff --git a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js index 7373e71681761..5b0798b737129 100644 --- a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js @@ -34,9 +34,7 @@ export type ResponseState = { placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, - opaqueIdentifierPrefix: string, nextSuspenseID: number, - nextOpaqueID: number, sentCompleteSegmentFunction: boolean, sentCompleteBoundaryFunction: boolean, sentClientRenderFunction: boolean, @@ -56,9 +54,7 @@ export function createResponseState( placeholderPrefix: responseState.placeholderPrefix, segmentPrefix: responseState.segmentPrefix, boundaryPrefix: responseState.boundaryPrefix, - opaqueIdentifierPrefix: responseState.opaqueIdentifierPrefix, nextSuspenseID: responseState.nextSuspenseID, - nextOpaqueID: responseState.nextOpaqueID, sentCompleteSegmentFunction: responseState.sentCompleteSegmentFunction, sentCompleteBoundaryFunction: responseState.sentCompleteBoundaryFunction, sentClientRenderFunction: responseState.sentClientRenderFunction, @@ -77,14 +73,12 @@ export function createRootFormatContext(): FormatContext { export type { FormatContext, SuspenseBoundaryID, - OpaqueIDType, } from './ReactDOMServerFormatConfig'; export { getChildFormatContext, UNINITIALIZED_SUSPENSE_BOUNDARY_ID, assignSuspenseBoundaryID, - makeServerID, pushStartInstance, pushEndInstance, pushStartCompletedSuspenseBoundary, diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 479a828a46e1f..1902dc65690c8 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -82,10 +82,6 @@ import {validateProperties as validateInputProperties} from '../shared/ReactDOMN import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; import hasOwnProperty from 'shared/hasOwnProperty'; -export type ServerOptions = { - identifierPrefix?: string, -}; - // Based on reading the React.Children implementation. TODO: type this somewhere? type ReactNode = string | number | ReactElement; type FlatReactChildren = Array; @@ -784,14 +780,7 @@ class ReactDOMServerRenderer { contextValueStack: Array; contextProviderStack: ?Array>; // DEV-only - uniqueID: number; - identifierPrefix: string; - - constructor( - children: mixed, - makeStaticMarkup: boolean, - options?: ServerOptions, - ) { + constructor(children: mixed, makeStaticMarkup: boolean) { const flatChildren = flattenTopLevelChildren(children); const topFrame: Frame = { @@ -820,10 +809,6 @@ class ReactDOMServerRenderer { this.contextStack = []; this.contextValueStack = []; - // useOpaqueIdentifier ID - this.uniqueID = 0; - this.identifierPrefix = (options && options.identifierPrefix) || ''; - if (__DEV__) { this.contextProviderStack = []; } diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index 26f2dd00ee0c6..2940c47bde46d 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -41,8 +41,6 @@ type Hook = {| next: Hook | null, |}; -type OpaqueIDType = string; - let currentlyRenderingComponent: Object | null = null; let firstWorkInProgressHook: Hook | null = null; let workInProgressHook: Hook | null = null; @@ -511,15 +509,7 @@ function useTransition(): [boolean, (callback: () => void) => void] { return [false, startTransition]; } -function useOpaqueIdentifier(): OpaqueIDType { - return ( - (currentPartialRenderer.identifierPrefix || '') + - 'R:' + - (currentPartialRenderer.uniqueID++).toString(36) - ); -} - -function useId(): OpaqueIDType { +function useId(): string { throw new Error('Not implemented.'); } @@ -552,7 +542,6 @@ export const Dispatcher: DispatcherType = { useDebugValue: noop, useDeferredValue, useTransition, - useOpaqueIdentifier, useId, // Subscriptions are not setup in a server environment. useMutableSource, diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index c71a9050211cf..727b782efd768 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -84,8 +84,6 @@ export type UpdatePayload = Object; export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; -export type OpaqueIDType = void; - export type RendererInspectionConfig = $ReadOnly<{| // Deprecated. Replaced with getInspectorDataForViewAtPoint. getInspectorDataForViewTag?: (tag: number) => Object, @@ -512,24 +510,6 @@ export function getInstanceFromNode(node: any) { throw new Error('Not yet implemented.'); } -export function isOpaqueHydratingObject(value: mixed): boolean { - throw new Error('Not yet implemented'); -} - -export function makeOpaqueHydratingObject( - attemptToReadValue: () => void, -): OpaqueIDType { - throw new Error('Not yet implemented.'); -} - -export function makeClientId(): OpaqueIDType { - throw new Error('Not yet implemented'); -} - -export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType { - throw new Error('Not yet implemented'); -} - export function beforeActiveInstanceBlur(internalInstanceHandle: Object) { // noop } diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index 769f46e852a2f..10c5e37f41bcc 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -43,7 +43,6 @@ export type ChildSet = void; // Unused export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; -export type OpaqueIDType = void; export type RendererInspectionConfig = $ReadOnly<{| // Deprecated. Replaced with getInspectorDataForViewAtPoint. @@ -499,24 +498,6 @@ export function getInstanceFromNode(node: any) { throw new Error('Not yet implemented.'); } -export function isOpaqueHydratingObject(value: mixed): boolean { - throw new Error('Not yet implemented'); -} - -export function makeOpaqueHydratingObject( - attemptToReadValue: () => void, -): OpaqueIDType { - throw new Error('Not yet implemented.'); -} - -export function makeClientId(): OpaqueIDType { - throw new Error('Not yet implemented'); -} - -export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType { - throw new Error('Not yet implemented'); -} - export function beforeActiveInstanceBlur(internalInstanceHandle: Object) { // noop } diff --git a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js index 3daa48f420b16..5b12d810bbe0f 100644 --- a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js +++ b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js @@ -59,14 +59,12 @@ SUSPENSE_UPDATE_TO_CLIENT_RENDER[0] = SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG; // Per response, export type ResponseState = { nextSuspenseID: number, - nextOpaqueID: number, }; // Allows us to keep track of what we've already written so we can refer back to it. export function createResponseState(): ResponseState { return { nextSuspenseID: 0, - nextOpaqueID: 0, }; } @@ -109,21 +107,6 @@ export function assignSuspenseBoundaryID( return responseState.nextSuspenseID++; } -export type OpaqueIDType = number; - -export function makeServerID( - responseState: null | ResponseState, -): OpaqueIDType { - if (responseState === null) { - throw new Error( - 'Invalid hook call. Hooks can only be called inside of the body of a function component.', - ); - } - - // TODO: This is not deterministic since it's created during render. - return responseState.nextOpaqueID++; -} - const RAW_TEXT = stringToPrecomputedChunk('RCTRawText'); export function pushTextInstance( diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index efcf727eac896..d217b16e50bfd 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -53,8 +53,6 @@ type Destination = { const POP = Buffer.from('/', 'utf8'); -let opaqueID = 0; - const ReactNoopServer = ReactFizzServer({ scheduleWork(callback: () => void) { callback(); @@ -88,10 +86,6 @@ const ReactNoopServer = ReactFizzServer({ return {state: 'pending', children: []}; }, - makeServerID(): number { - return opaqueID++; - }, - getChildFormatContext(): null { return null; }, diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index a1d7009a85a43..1238d673725ac 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -17,7 +17,6 @@ import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.new'; import type {HookFlags} from './ReactHookEffectTags'; import type {FiberRoot} from './ReactInternalTypes'; -import type {OpaqueIDType} from './ReactFiberHostConfig'; import type {Cache} from './ReactFiberCacheComponent.new'; import type {Flags} from './ReactFiberFlags'; @@ -95,18 +94,12 @@ import { checkIfWorkInProgressReceivedUpdate, } from './ReactFiberBeginWork.new'; import {getIsHydrating} from './ReactFiberHydrationContext.new'; -import { - makeClientId, - makeClientIdInDEV, - makeOpaqueHydratingObject, -} from './ReactFiberHostConfig'; import { getWorkInProgressVersion, markSourceAsDirty, setWorkInProgressVersion, warnAboutMultipleRenderersDEV, } from './ReactMutableSource.new'; -import {getIsRendering} from './ReactCurrentFiber'; import {logStateUpdateScheduled} from './DebugTracing'; import {markStateUpdateScheduled} from './SchedulingProfiler'; import {createCache, CacheContext} from './ReactFiberCacheComponent.new'; @@ -139,10 +132,8 @@ export type UpdateQueue = {| |}; let didWarnAboutMismatchedHooksForComponent; -let didWarnAboutUseOpaqueIdentifier; let didWarnUncachedGetSnapshot; if (__DEV__) { - didWarnAboutUseOpaqueIdentifier = {}; didWarnAboutMismatchedHooksForComponent = new Set(); } @@ -2045,94 +2036,6 @@ export function getIsUpdatingOpaqueValueInRenderPhaseInDEV(): boolean | void { } } -function warnOnOpaqueIdentifierAccessInDEV(fiber) { - if (__DEV__) { - // TODO: Should warn in effects and callbacks, too - const name = getComponentNameFromFiber(fiber) || 'Unknown'; - if (getIsRendering() && !didWarnAboutUseOpaqueIdentifier[name]) { - console.error( - 'The object passed back from useOpaqueIdentifier is meant to be ' + - 'passed through to attributes only. Do not read the ' + - 'value directly.', - ); - didWarnAboutUseOpaqueIdentifier[name] = true; - } - } -} - -function mountOpaqueIdentifier(): OpaqueIDType | void { - const makeId = __DEV__ - ? makeClientIdInDEV.bind( - null, - warnOnOpaqueIdentifierAccessInDEV.bind(null, currentlyRenderingFiber), - ) - : makeClientId; - - if (getIsHydrating()) { - let didUpgrade = false; - const fiber = currentlyRenderingFiber; - const readValue = () => { - if (!didUpgrade) { - // Only upgrade once. This works even inside the render phase because - // the update is added to a shared queue, which outlasts the - // in-progress render. - didUpgrade = true; - if (__DEV__) { - isUpdatingOpaqueValueInRenderPhase = true; - setId(makeId()); - isUpdatingOpaqueValueInRenderPhase = false; - warnOnOpaqueIdentifierAccessInDEV(fiber); - } else { - setId(makeId()); - } - } - - throw new Error( - 'The object passed back from useOpaqueIdentifier is meant to be ' + - 'passed through to attributes only. Do not read the value directly.', - ); - }; - const id = makeOpaqueHydratingObject(readValue); - - const setId = mountState(id)[1]; - - if ((currentlyRenderingFiber.mode & ConcurrentMode) === NoMode) { - if ( - __DEV__ && - enableStrictEffects && - (currentlyRenderingFiber.mode & StrictEffectsMode) === NoMode - ) { - currentlyRenderingFiber.flags |= MountPassiveDevEffect | PassiveEffect; - } else { - currentlyRenderingFiber.flags |= PassiveEffect; - } - pushEffect( - HookHasEffect | HookPassive, - () => { - setId(makeId()); - }, - undefined, - null, - ); - } - return id; - } else { - const id = makeId(); - mountState(id); - return id; - } -} - -function updateOpaqueIdentifier(): OpaqueIDType | void { - const id = updateState(undefined)[0]; - return id; -} - -function rerenderOpaqueIdentifier(): OpaqueIDType | void { - const id = rerenderState(undefined)[0]; - return id; -} - function mountId(): string { const hook = mountWorkInProgressHook(); @@ -2481,7 +2384,6 @@ export const ContextOnlyDispatcher: Dispatcher = { useTransition: throwInvalidHookError, useMutableSource: throwInvalidHookError, useSyncExternalStore: throwInvalidHookError, - useOpaqueIdentifier: throwInvalidHookError, useId: throwInvalidHookError, unstable_isNewReconciler: enableNewReconciler, @@ -2510,7 +2412,6 @@ const HooksDispatcherOnMount: Dispatcher = { useTransition: mountTransition, useMutableSource: mountMutableSource, useSyncExternalStore: mountSyncExternalStore, - useOpaqueIdentifier: mountOpaqueIdentifier, useId: mountId, unstable_isNewReconciler: enableNewReconciler, @@ -2539,7 +2440,6 @@ const HooksDispatcherOnUpdate: Dispatcher = { useTransition: updateTransition, useMutableSource: updateMutableSource, useSyncExternalStore: updateSyncExternalStore, - useOpaqueIdentifier: updateOpaqueIdentifier, useId: updateId, unstable_isNewReconciler: enableNewReconciler, @@ -2568,7 +2468,6 @@ const HooksDispatcherOnRerender: Dispatcher = { useTransition: rerenderTransition, useMutableSource: updateMutableSource, useSyncExternalStore: mountSyncExternalStore, - useOpaqueIdentifier: rerenderOpaqueIdentifier, useId: updateId, unstable_isNewReconciler: enableNewReconciler, @@ -2736,11 +2635,6 @@ if (__DEV__) { mountHookTypesDev(); return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - mountHookTypesDev(); - return mountOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; mountHookTypesDev(); @@ -2883,11 +2777,6 @@ if (__DEV__) { updateHookTypesDev(); return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - updateHookTypesDev(); - return mountOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; updateHookTypesDev(); @@ -3030,11 +2919,6 @@ if (__DEV__) { updateHookTypesDev(); return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - updateHookTypesDev(); - return updateOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; updateHookTypesDev(); @@ -3178,11 +3062,6 @@ if (__DEV__) { updateHookTypesDev(); return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - updateHookTypesDev(); - return rerenderOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; updateHookTypesDev(); @@ -3341,12 +3220,6 @@ if (__DEV__) { mountHookTypesDev(); return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - warnInvalidHookAccess(); - mountHookTypesDev(); - return mountOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; warnInvalidHookAccess(); @@ -3506,12 +3379,6 @@ if (__DEV__) { updateHookTypesDev(); return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return updateOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; warnInvalidHookAccess(); @@ -3672,12 +3539,6 @@ if (__DEV__) { updateHookTypesDev(); return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return rerenderOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; warnInvalidHookAccess(); diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 167698271dbac..3b0b87cb1ab7c 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -17,7 +17,6 @@ import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.old'; import type {HookFlags} from './ReactHookEffectTags'; import type {FiberRoot} from './ReactInternalTypes'; -import type {OpaqueIDType} from './ReactFiberHostConfig'; import type {Cache} from './ReactFiberCacheComponent.old'; import type {Flags} from './ReactFiberFlags'; @@ -95,18 +94,12 @@ import { checkIfWorkInProgressReceivedUpdate, } from './ReactFiberBeginWork.old'; import {getIsHydrating} from './ReactFiberHydrationContext.old'; -import { - makeClientId, - makeClientIdInDEV, - makeOpaqueHydratingObject, -} from './ReactFiberHostConfig'; import { getWorkInProgressVersion, markSourceAsDirty, setWorkInProgressVersion, warnAboutMultipleRenderersDEV, } from './ReactMutableSource.old'; -import {getIsRendering} from './ReactCurrentFiber'; import {logStateUpdateScheduled} from './DebugTracing'; import {markStateUpdateScheduled} from './SchedulingProfiler'; import {createCache, CacheContext} from './ReactFiberCacheComponent.old'; @@ -139,10 +132,8 @@ export type UpdateQueue = {| |}; let didWarnAboutMismatchedHooksForComponent; -let didWarnAboutUseOpaqueIdentifier; let didWarnUncachedGetSnapshot; if (__DEV__) { - didWarnAboutUseOpaqueIdentifier = {}; didWarnAboutMismatchedHooksForComponent = new Set(); } @@ -2045,94 +2036,6 @@ export function getIsUpdatingOpaqueValueInRenderPhaseInDEV(): boolean | void { } } -function warnOnOpaqueIdentifierAccessInDEV(fiber) { - if (__DEV__) { - // TODO: Should warn in effects and callbacks, too - const name = getComponentNameFromFiber(fiber) || 'Unknown'; - if (getIsRendering() && !didWarnAboutUseOpaqueIdentifier[name]) { - console.error( - 'The object passed back from useOpaqueIdentifier is meant to be ' + - 'passed through to attributes only. Do not read the ' + - 'value directly.', - ); - didWarnAboutUseOpaqueIdentifier[name] = true; - } - } -} - -function mountOpaqueIdentifier(): OpaqueIDType | void { - const makeId = __DEV__ - ? makeClientIdInDEV.bind( - null, - warnOnOpaqueIdentifierAccessInDEV.bind(null, currentlyRenderingFiber), - ) - : makeClientId; - - if (getIsHydrating()) { - let didUpgrade = false; - const fiber = currentlyRenderingFiber; - const readValue = () => { - if (!didUpgrade) { - // Only upgrade once. This works even inside the render phase because - // the update is added to a shared queue, which outlasts the - // in-progress render. - didUpgrade = true; - if (__DEV__) { - isUpdatingOpaqueValueInRenderPhase = true; - setId(makeId()); - isUpdatingOpaqueValueInRenderPhase = false; - warnOnOpaqueIdentifierAccessInDEV(fiber); - } else { - setId(makeId()); - } - } - - throw new Error( - 'The object passed back from useOpaqueIdentifier is meant to be ' + - 'passed through to attributes only. Do not read the value directly.', - ); - }; - const id = makeOpaqueHydratingObject(readValue); - - const setId = mountState(id)[1]; - - if ((currentlyRenderingFiber.mode & ConcurrentMode) === NoMode) { - if ( - __DEV__ && - enableStrictEffects && - (currentlyRenderingFiber.mode & StrictEffectsMode) === NoMode - ) { - currentlyRenderingFiber.flags |= MountPassiveDevEffect | PassiveEffect; - } else { - currentlyRenderingFiber.flags |= PassiveEffect; - } - pushEffect( - HookHasEffect | HookPassive, - () => { - setId(makeId()); - }, - undefined, - null, - ); - } - return id; - } else { - const id = makeId(); - mountState(id); - return id; - } -} - -function updateOpaqueIdentifier(): OpaqueIDType | void { - const id = updateState(undefined)[0]; - return id; -} - -function rerenderOpaqueIdentifier(): OpaqueIDType | void { - const id = rerenderState(undefined)[0]; - return id; -} - function mountId(): string { const hook = mountWorkInProgressHook(); @@ -2481,7 +2384,6 @@ export const ContextOnlyDispatcher: Dispatcher = { useTransition: throwInvalidHookError, useMutableSource: throwInvalidHookError, useSyncExternalStore: throwInvalidHookError, - useOpaqueIdentifier: throwInvalidHookError, useId: throwInvalidHookError, unstable_isNewReconciler: enableNewReconciler, @@ -2510,7 +2412,6 @@ const HooksDispatcherOnMount: Dispatcher = { useTransition: mountTransition, useMutableSource: mountMutableSource, useSyncExternalStore: mountSyncExternalStore, - useOpaqueIdentifier: mountOpaqueIdentifier, useId: mountId, unstable_isNewReconciler: enableNewReconciler, @@ -2539,7 +2440,6 @@ const HooksDispatcherOnUpdate: Dispatcher = { useTransition: updateTransition, useMutableSource: updateMutableSource, useSyncExternalStore: updateSyncExternalStore, - useOpaqueIdentifier: updateOpaqueIdentifier, useId: updateId, unstable_isNewReconciler: enableNewReconciler, @@ -2568,7 +2468,6 @@ const HooksDispatcherOnRerender: Dispatcher = { useTransition: rerenderTransition, useMutableSource: updateMutableSource, useSyncExternalStore: mountSyncExternalStore, - useOpaqueIdentifier: rerenderOpaqueIdentifier, useId: updateId, unstable_isNewReconciler: enableNewReconciler, @@ -2736,11 +2635,6 @@ if (__DEV__) { mountHookTypesDev(); return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - mountHookTypesDev(); - return mountOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; mountHookTypesDev(); @@ -2883,11 +2777,6 @@ if (__DEV__) { updateHookTypesDev(); return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - updateHookTypesDev(); - return mountOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; updateHookTypesDev(); @@ -3030,11 +2919,6 @@ if (__DEV__) { updateHookTypesDev(); return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - updateHookTypesDev(); - return updateOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; updateHookTypesDev(); @@ -3178,11 +3062,6 @@ if (__DEV__) { updateHookTypesDev(); return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - updateHookTypesDev(); - return rerenderOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; updateHookTypesDev(); @@ -3341,12 +3220,6 @@ if (__DEV__) { mountHookTypesDev(); return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - warnInvalidHookAccess(); - mountHookTypesDev(); - return mountOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; warnInvalidHookAccess(); @@ -3506,12 +3379,6 @@ if (__DEV__) { updateHookTypesDev(); return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return updateOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; warnInvalidHookAccess(); @@ -3672,12 +3539,6 @@ if (__DEV__) { updateHookTypesDev(); return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); }, - useOpaqueIdentifier(): OpaqueIDType | void { - currentHookNameInDev = 'useOpaqueIdentifier'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return rerenderOpaqueIdentifier(); - }, useId(): string { currentHookNameInDev = 'useId'; warnInvalidHookAccess(); diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index e35e5515bb0bf..71bf8175105d6 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -474,8 +474,7 @@ export function scheduleUpdateOnFiber( // if the update originates from user space (with the exception of local // hook updates, which are handled differently and don't reach this // function), but there are some internal React features that use this as - // an implementation detail, like selective hydration - // and useOpaqueIdentifier. + // an implementation detail, like selective hydration. warnAboutRenderPhaseUpdatesInDEV(fiber); // Track lanes that were updated during the render phase @@ -898,12 +897,10 @@ function recoverFromConcurrentError(root, errorRetryLanes) { exitStatus === RootErrored && workInProgressRootRenderPhaseUpdatedLanes !== NoLanes ) { - // There was a render phase update during this render. This was likely a - // useOpaqueIdentifier hook upgrading itself to a client ID. Try rendering - // again. This time, the component will use a client ID and will proceed - // without throwing. If multiple IDs upgrade as a result of the same - // update, we will have to do multiple render passes. To protect against - // an inifinite loop, eventually we'll give up. + // There was a render phase update during this render. Some internal React + // implementation details may use this as a trick to schedule another + // render pass. To protect against an inifinite loop, eventually + // we'll give up. continue; } break; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index cde46adb01ab0..8be4630dc079d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -474,8 +474,7 @@ export function scheduleUpdateOnFiber( // if the update originates from user space (with the exception of local // hook updates, which are handled differently and don't reach this // function), but there are some internal React features that use this as - // an implementation detail, like selective hydration - // and useOpaqueIdentifier. + // an implementation detail, like selective hydration. warnAboutRenderPhaseUpdatesInDEV(fiber); // Track lanes that were updated during the render phase @@ -898,12 +897,10 @@ function recoverFromConcurrentError(root, errorRetryLanes) { exitStatus === RootErrored && workInProgressRootRenderPhaseUpdatedLanes !== NoLanes ) { - // There was a render phase update during this render. This was likely a - // useOpaqueIdentifier hook upgrading itself to a client ID. Try rendering - // again. This time, the component will use a client ID and will proceed - // without throwing. If multiple IDs upgrade as a result of the same - // update, we will have to do multiple render passes. To protect against - // an inifinite loop, eventually we'll give up. + // There was a render phase update during this render. Some internal React + // implementation details may use this as a trick to schedule another + // render pass. To protect against an inifinite loop, eventually + // we'll give up. continue; } break; diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 9dac4f40aad53..db964103836da 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -43,7 +43,6 @@ export type HookType = | 'useTransition' | 'useMutableSource' | 'useSyncExternalStore' - | 'useOpaqueIdentifier' | 'useId' | 'useCacheRefresh'; @@ -317,7 +316,6 @@ export type Dispatcher = {| getSnapshot: () => T, getServerSnapshot?: () => T, ): T, - useOpaqueIdentifier(): any, useId(): string, useCacheRefresh?: () => (?() => T, ?T) => void, diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js index cc842bd16d22f..55ce08b45450b 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js @@ -1916,14 +1916,14 @@ describe('ReactIncrementalErrorHandling', () => { }); it("does not infinite loop if there's a render phase update in the same render as an error", async () => { - // useOpaqueIdentifier uses an render phase update as an implementation - // detail. When an error is accompanied by a render phase update, we assume - // that it comes from useOpaqueIdentifier, because render phase updates - // triggered from userspace are not allowed (we log a warning). So we keep - // attempting to recover until no more opaque identifiers need to be - // upgraded. However, we should give up after some point to prevent an - // infinite loop in the case where there is (by accident) a render phase - // triggered from userspace. + // Some React features may schedule a render phase update as an + // implementation detail. When an error is accompanied by a render phase + // update, we assume that it comes from React internals, because render + // phase updates triggered from userspace are not allowed (we log a + // warning). So we keep attempting to recover until no more opaque + // identifiers need to be upgraded. However, we should give up after some + // point to prevent an infinite loop in the case where there is (by + // accident) a render phase triggered from userspace. spyOnDev(console, 'error'); diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index 29d4fd7e79893..6535d8d3fdec3 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -38,7 +38,6 @@ export opaque type ChildSet = mixed; // eslint-disable-line no-undef export opaque type TimeoutHandle = mixed; // eslint-disable-line no-undef export opaque type NoTimeout = mixed; // eslint-disable-line no-undef export opaque type RendererInspectionConfig = mixed; // eslint-disable-line no-undef -export opaque type OpaqueIDType = mixed; export type EventResponder = any; export const getPublicInstance = $$$hostConfig.getPublicInstance; @@ -62,11 +61,6 @@ export const supportsMutation = $$$hostConfig.supportsMutation; export const supportsPersistence = $$$hostConfig.supportsPersistence; export const supportsHydration = $$$hostConfig.supportsHydration; export const getInstanceFromNode = $$$hostConfig.getInstanceFromNode; -export const isOpaqueHydratingObject = $$$hostConfig.isOpaqueHydratingObject; -export const makeOpaqueHydratingObject = - $$$hostConfig.makeOpaqueHydratingObject; -export const makeClientId = $$$hostConfig.makeClientId; -export const makeClientIdInDEV = $$$hostConfig.makeClientIdInDEV; export const beforeActiveInstanceBlur = $$$hostConfig.beforeActiveInstanceBlur; export const afterActiveInstanceBlur = $$$hostConfig.afterActiveInstanceBlur; export const preparePortalMount = $$$hostConfig.preparePortalMount; diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index bf1a2d56e0f93..7b31e6b37ed9e 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -16,14 +16,12 @@ import type { ReactContext, } from 'shared/ReactTypes'; -import type {ResponseState, OpaqueIDType} from './ReactServerFormatConfig'; +import type {ResponseState} from './ReactServerFormatConfig'; import type {Task} from './ReactFizzServer'; import {readContext as readContextImpl} from './ReactFizzNewContext'; import {getTreeId} from './ReactFizzTreeContext'; -import {makeServerID} from './ReactServerFormatConfig'; - import {enableCache} from 'shared/ReactFeatureFlags'; import is from 'shared/objectIs'; @@ -509,10 +507,6 @@ function useTransition(): [boolean, (callback: () => void) => void] { return [false, unsupportedStartTransition]; } -function useOpaqueIdentifier(): OpaqueIDType { - return makeServerID(currentResponseState); -} - function useId(): string { const task: Task = (currentlyRenderingTask: any); const treeId = getTreeId(task.treeContext); @@ -559,7 +553,6 @@ export const Dispatcher: DispatcherType = { useDebugValue: noop, useDeferredValue, useTransition, - useOpaqueIdentifier, useId, // Subscriptions are not setup in a server environment. useMutableSource, diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index bba34065cc268..418fbb8241a66 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -845,7 +845,6 @@ const Dispatcher: DispatcherType = { useLayoutEffect: (unsupportedHook: any), useImperativeHandle: (unsupportedHook: any), useEffect: (unsupportedHook: any), - useOpaqueIdentifier: (unsupportedHook: any), useId: (unsupportedHook: any), useMutableSource: (unsupportedHook: any), useSyncExternalStore: (unsupportedHook: any), diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js index 8cfe59ce1628c..f246ddab8b5bf 100644 --- a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js @@ -28,7 +28,6 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef export opaque type ResponseState = mixed; export opaque type FormatContext = mixed; export opaque type SuspenseBoundaryID = mixed; -export opaque type OpaqueIDType = mixed; export const isPrimaryRenderer = false; @@ -36,7 +35,6 @@ export const getChildFormatContext = $$$hostConfig.getChildFormatContext; export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID = $$$hostConfig.UNINITIALIZED_SUSPENSE_BOUNDARY_ID; export const assignSuspenseBoundaryID = $$$hostConfig.assignSuspenseBoundaryID; -export const makeServerID = $$$hostConfig.makeServerID; export const pushTextInstance = $$$hostConfig.pushTextInstance; export const pushStartInstance = $$$hostConfig.pushStartInstance; export const pushEndInstance = $$$hostConfig.pushEndInstance; diff --git a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js index 731124ea51593..d75f1f7c9b453 100644 --- a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js +++ b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js @@ -42,7 +42,6 @@ export function waitForSuspense(fn: () => T): Promise { useDebugValue: unsupported, useDeferredValue: unsupported, useTransition: unsupported, - useOpaqueIdentifier: unsupported, useId: unsupported, useMutableSource: unsupported, useSyncExternalStore: unsupported, diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index 6cdadabe281a5..503b08efaf20b 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -7,7 +7,6 @@ * @flow */ -import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols'; import isArray from 'shared/isArray'; import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities'; @@ -40,12 +39,6 @@ export type ChildSet = void; // Unused export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; export type EventResponder = any; -export opaque type OpaqueIDType = - | string - | { - toString: () => string | void, - valueOf: () => string | void, - }; export type RendererInspectionConfig = $ReadOnly<{||}>; @@ -298,43 +291,6 @@ export function getInstanceFromNode(mockNode: Object) { return null; } -let clientId: number = 0; -export function makeClientId(): OpaqueIDType { - return 'c_' + (clientId++).toString(36); -} - -export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType { - const id = 'c_' + (clientId++).toString(36); - return { - toString() { - warnOnAccessInDEV(); - return id; - }, - valueOf() { - warnOnAccessInDEV(); - return id; - }, - }; -} - -export function isOpaqueHydratingObject(value: mixed): boolean { - return ( - value !== null && - typeof value === 'object' && - value.$$typeof === REACT_OPAQUE_ID_TYPE - ); -} - -export function makeOpaqueHydratingObject( - attemptToReadValue: () => void, -): OpaqueIDType { - return { - $$typeof: REACT_OPAQUE_ID_TYPE, - toString: attemptToReadValue, - valueOf: attemptToReadValue, - }; -} - export function beforeActiveInstanceBlur(internalInstanceHandle: Object) { // noop } diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index a2e678c580b2e..70bbc6112a56c 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -40,7 +40,6 @@ export { unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, - unstable_useOpaqueIdentifier, unstable_useId, useCallback, useContext, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index 20c22828583f5..bc3f4883bdb3d 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -36,7 +36,6 @@ export { unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, - unstable_useOpaqueIdentifier, unstable_useId, useCallback, useContext, diff --git a/packages/react/index.js b/packages/react/index.js index 3108c06c55284..a27507524e50b 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -61,7 +61,6 @@ export { unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, - unstable_useOpaqueIdentifier, unstable_useId, useCallback, useContext, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index eef99fdabf2a4..922acfc158109 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -39,7 +39,6 @@ export { unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, - unstable_useOpaqueIdentifier, unstable_useId, useCallback, useContext, diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index 517dc3f8fb2db..eb9673efd16ad 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -29,7 +29,6 @@ export { lazy, memo, startTransition, - unstable_useOpaqueIdentifier, unstable_useId, useCallback, useContext, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 868538c83f59b..5fe5b4d96b609 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -52,7 +52,6 @@ import { useState, useTransition, useDeferredValue, - useOpaqueIdentifier, useId, useCacheRefresh, } from './ReactHooks'; @@ -127,7 +126,6 @@ export { REACT_CACHE_TYPE as unstable_Cache, // enableScopeAPI REACT_SCOPE_TYPE as unstable_Scope, - useOpaqueIdentifier as unstable_useOpaqueIdentifier, useId as unstable_useId, act, }; diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 1f987de1671ba..12a5350083f38 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -14,7 +14,6 @@ import type { MutableSourceSubscribeFn, ReactContext, } from 'shared/ReactTypes'; -import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig'; import ReactCurrentDispatcher from './ReactCurrentDispatcher'; @@ -169,11 +168,6 @@ export function useDeferredValue(value: T): T { return dispatcher.useDeferredValue(value); } -export function useOpaqueIdentifier(): OpaqueIDType | void { - const dispatcher = resolveDispatcher(); - return dispatcher.useOpaqueIdentifier(); -} - export function useId(): string { const dispatcher = resolveDispatcher(); return dispatcher.useId(); diff --git a/packages/react/unstable-shared-subset.experimental.js b/packages/react/unstable-shared-subset.experimental.js index d556400750b4c..7a9352f30199a 100644 --- a/packages/react/unstable-shared-subset.experimental.js +++ b/packages/react/unstable-shared-subset.experimental.js @@ -27,7 +27,6 @@ export { unstable_DebugTracingMode, unstable_getCacheSignal, unstable_getCacheForType, - unstable_useOpaqueIdentifier, unstable_useId, useCallback, useContext, diff --git a/packages/shared/CheckStringCoercion.js b/packages/shared/CheckStringCoercion.js index 9db9e7145ad4f..305572f8462c7 100644 --- a/packages/shared/CheckStringCoercion.js +++ b/packages/shared/CheckStringCoercion.js @@ -7,8 +7,6 @@ * @flow */ -import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols'; - /* * The `'' + value` pattern (used in in perf-sensitive code) throws for Symbol * and Temporal.* types. See https://github.com/facebook/react/pull/22064. @@ -35,16 +33,6 @@ function typeName(value: mixed): string { // $FlowFixMe only called in DEV, so void return is not possible. function willCoercionThrow(value: mixed): boolean { if (__DEV__) { - if ( - value !== null && - typeof value === 'object' && - value.$$typeof === REACT_OPAQUE_ID_TYPE - ) { - // OpaqueID type is expected to throw, so React will handle it. Not sure if - // it's expected that string coercion will throw, but we'll assume it's OK. - // See https://github.com/facebook/react/issues/20127. - return; - } try { testStringCoercion(value); return false; diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index c2fcba6324410..a50a06b150169 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -26,7 +26,6 @@ export let REACT_SUSPENSE_LIST_TYPE = 0xead8; export let REACT_MEMO_TYPE = 0xead3; export let REACT_LAZY_TYPE = 0xead4; export let REACT_SCOPE_TYPE = 0xead7; -export let REACT_OPAQUE_ID_TYPE = 0xeae0; export let REACT_DEBUG_TRACING_MODE_TYPE = 0xeae1; export let REACT_OFFSCREEN_TYPE = 0xeae2; export let REACT_LEGACY_HIDDEN_TYPE = 0xeae3; @@ -47,7 +46,6 @@ if (typeof Symbol === 'function' && Symbol.for) { REACT_MEMO_TYPE = symbolFor('react.memo'); REACT_LAZY_TYPE = symbolFor('react.lazy'); REACT_SCOPE_TYPE = symbolFor('react.scope'); - REACT_OPAQUE_ID_TYPE = symbolFor('react.opaque.id'); REACT_DEBUG_TRACING_MODE_TYPE = symbolFor('react.debug_trace_mode'); REACT_OFFSCREEN_TYPE = symbolFor('react.offscreen'); REACT_LEGACY_HIDDEN_TYPE = symbolFor('react.legacy_hidden'); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index aab5d62476cc3..93391eb309d49 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -342,7 +342,7 @@ "352": "React Lazy Components are not yet supported on the server.", "353": "A server block should never encode any other slots. This is a bug in React.", "354": "getInspectorDataForViewAtPoint() is not available in production.", - "355": "The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.", + "355": "The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly. (TODO: This feature was never released so we should be able to remove this error from the map.)", "356": "Could not read the cache.", "357": "The current renderer does not support React Scopes. This error is likely caused by a bug in React. Please file an issue.", "358": "Invalid update priority: %s. This is a bug in React.", From 5cccacd131242bdea2c2fe4b33fac50d2e3132b4 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 1 Nov 2021 18:26:42 -0400 Subject: [PATCH 076/109] Upgrade useId to alpha channel (#22674) --- .../src/__tests__/ReactHooksInspectionIntegration-test.js | 2 +- packages/react-dom/src/__tests__/ReactDOMUseId-test.js | 2 +- packages/react/index.classic.fb.js | 2 +- packages/react/index.experimental.js | 2 +- packages/react/index.js | 2 +- packages/react/index.modern.fb.js | 2 +- packages/react/index.stable.js | 2 +- packages/react/src/React.js | 2 +- packages/react/unstable-shared-subset.experimental.js | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 559a0bf75e52f..5b36039ecc5dc 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -600,7 +600,7 @@ describe('ReactHooksInspectionIntegration', () => { it('should support useId hook', () => { function Foo(props) { - const id = React.unstable_useId(); + const id = React.useId(); const [state] = React.useState('hello'); return
{state}
; } diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index b61e79fa670d6..09ccdc86d14e6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -34,7 +34,7 @@ describe('useId', () => { ReactDOMFizzServer = require('react-dom/server'); Stream = require('stream'); Suspense = React.Suspense; - useId = React.unstable_useId; + useId = React.useId; // Test Environment const jsdom = new JSDOM( diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 70bbc6112a56c..33c770f00fed5 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -40,7 +40,7 @@ export { unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, - unstable_useId, + useId, useCallback, useContext, useDebugValue, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index bc3f4883bdb3d..d4dc33a4db01a 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -36,7 +36,7 @@ export { unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, - unstable_useId, + useId, useCallback, useContext, useDebugValue, diff --git a/packages/react/index.js b/packages/react/index.js index a27507524e50b..6a249ba432c72 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -61,7 +61,7 @@ export { unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, - unstable_useId, + useId, useCallback, useContext, useDebugValue, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 922acfc158109..5b0d75e21460b 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -39,7 +39,7 @@ export { unstable_getCacheSignal, unstable_getCacheForType, unstable_useCacheRefresh, - unstable_useId, + useId, useCallback, useContext, useDebugValue, diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index eb9673efd16ad..234ec8336a9ed 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -29,7 +29,7 @@ export { lazy, memo, startTransition, - unstable_useId, + useId, useCallback, useContext, useDebugValue, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 5fe5b4d96b609..891269ee32653 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -126,6 +126,6 @@ export { REACT_CACHE_TYPE as unstable_Cache, // enableScopeAPI REACT_SCOPE_TYPE as unstable_Scope, - useId as unstable_useId, + useId, act, }; diff --git a/packages/react/unstable-shared-subset.experimental.js b/packages/react/unstable-shared-subset.experimental.js index 7a9352f30199a..a24e302e49c4a 100644 --- a/packages/react/unstable-shared-subset.experimental.js +++ b/packages/react/unstable-shared-subset.experimental.js @@ -27,7 +27,7 @@ export { unstable_DebugTracingMode, unstable_getCacheSignal, unstable_getCacheForType, - unstable_useId, + useId, useCallback, useContext, useDebugValue, From 3fcd81dd1c3cd1413b72ae2919bc8312787f8f58 Mon Sep 17 00:00:00 2001 From: Abhay Gupta <51379307+akgupta0777@users.noreply.github.com> Date: Tue, 2 Nov 2021 20:00:37 +0530 Subject: [PATCH 077/109] Improved workers filenames in devtools-inline (#22676) --- packages/react-devtools-inline/webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index bc38a8792e4c4..7c2325bf70f45 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -95,6 +95,7 @@ module.exports = { loader: 'workerize-loader', options: { inline: true, + name: '[name]', }, }, { From 9fb3442250cf198782a08024195fcc5402479160 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 2 Nov 2021 11:02:45 -0400 Subject: [PATCH 078/109] Fix DevTools advanced tooltip display conditional check (#22669) --- .../src/devtools/views/Profiler/SnapshotCommitList.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitList.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitList.js index 4fda4177706ad..2b837f28f6495 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitList.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitList.js @@ -217,7 +217,11 @@ function List({ // Only some React versions include commit durations. // Show a richer tooltip only for builds that have that info. - if (effectDuration !== null || passiveEffectDuration !== null) { + if ( + effectDuration !== null || + passiveEffectDuration !== null || + priorityLevel !== null + ) { tooltipLabel = (
    {priorityLevel !== null && ( From 00ced1e2b7610543a519329a76ad0bfd12cd1c32 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 2 Nov 2021 20:59:17 -0400 Subject: [PATCH 079/109] Fix useId in strict mode (#22681) * Fix: useId in strict mode In strict mode, `renderWithHooks` is called twice to flush out side effects. Modying the tree context (`pushTreeId` and `pushTreeFork`) is effectful, so before this fix, the tree context was allocating two slots for a materialized id instead of one. To address, I lifted those calls outside of `renderWithHooks`. This is how I had originally structured it, and it's how Fizz is structured, too. The other solution would be to reset the stack in between the calls but that's also a bit weird because we usually only ever reset the stack during unwind or complete. * Add test for render phase updates Noticed this while fixing the previous bug --- .../src/__tests__/ReactDOMUseId-test.js | 57 +++++++++++++++++++ .../src/ReactFiberBeginWork.new.js | 31 +++++++++- .../src/ReactFiberBeginWork.old.js | 31 +++++++++- .../src/ReactFiberHooks.new.js | 30 +++++----- .../src/ReactFiberHooks.old.js | 30 +++++----- .../src/ReactFiberTreeContext.new.js | 14 +++++ .../src/ReactFiberTreeContext.old.js | 14 +++++ packages/react-server/src/ReactFizzHooks.js | 1 + 8 files changed, 172 insertions(+), 36 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index 09ccdc86d14e6..dd7bbaa8b41fa 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -16,6 +16,7 @@ let ReactDOMFizzServer; let Stream; let Suspense; let useId; +let useState; let document; let writable; let container; @@ -35,6 +36,7 @@ describe('useId', () => { Stream = require('stream'); Suspense = React.Suspense; useId = React.useId; + useState = React.useState; // Test Environment const jsdom = new JSDOM( @@ -198,6 +200,35 @@ describe('useId', () => { `); }); + test('StrictMode double rendering', async () => { + const {StrictMode} = React; + + function App() { + return ( + + + + ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + await clientAct(async () => { + ReactDOM.hydrateRoot(container, ); + }); + expect(container).toMatchInlineSnapshot(` +
    +
    +
    + `); + }); + test('empty (null) children', async () => { // We don't treat empty children different from non-empty ones, which means // they get allocated a slot when generating ids. There's no inherent reason @@ -313,6 +344,32 @@ describe('useId', () => { `); }); + test('local render phase updates', async () => { + function App({swap}) { + const [count, setCount] = useState(0); + if (count < 3) { + setCount(count + 1); + } + return useId(); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + await clientAct(async () => { + ReactDOM.hydrateRoot(container, ); + }); + expect(container).toMatchInlineSnapshot(` +
    + R:0 + +
    + `); + }); + test('basic incremental hydration', async () => { function App() { return ( diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 653ee9e1b4ea7..d66bba0dc2ee3 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -174,7 +174,11 @@ import { prepareToReadContext, scheduleWorkOnParentPath, } from './ReactFiberNewContext.new'; -import {renderWithHooks, bailoutHooks} from './ReactFiberHooks.new'; +import { + renderWithHooks, + checkDidRenderIdHook, + bailoutHooks, +} from './ReactFiberHooks.new'; import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.new'; import { getMaskedContext, @@ -240,6 +244,7 @@ import { getForksAtLevel, isForkedChild, pushTreeId, + pushMaterializedTreeId, } from './ReactFiberTreeContext.new'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -365,6 +370,7 @@ function updateForwardRef( // The rest is a fork of updateFunctionComponent let nextChildren; + let hasId; prepareToReadContext(workInProgress, renderLanes); if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -380,6 +386,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictLegacyMode @@ -394,6 +401,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } @@ -408,6 +416,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -418,6 +427,10 @@ function updateForwardRef( return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } + if (getIsHydrating() && hasId) { + pushMaterializedTreeId(workInProgress); + } + // React DevTools reads this flag. workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren, renderLanes); @@ -970,6 +983,7 @@ function updateFunctionComponent( } let nextChildren; + let hasId; prepareToReadContext(workInProgress, renderLanes); if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -985,6 +999,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictLegacyMode @@ -999,6 +1014,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } @@ -1013,6 +1029,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -1023,6 +1040,10 @@ function updateFunctionComponent( return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } + if (getIsHydrating() && hasId) { + pushMaterializedTreeId(workInProgress); + } + // React DevTools reads this flag. workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren, renderLanes); @@ -1593,6 +1614,7 @@ function mountIndeterminateComponent( prepareToReadContext(workInProgress, renderLanes); let value; + let hasId; if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -1629,6 +1651,7 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); setIsRendering(false); } else { value = renderWithHooks( @@ -1639,6 +1662,7 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -1758,12 +1782,17 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } } } + if (getIsHydrating() && hasId) { + pushMaterializedTreeId(workInProgress); + } + reconcileChildren(null, workInProgress, value, renderLanes); if (__DEV__) { validateFunctionComponentInDev(workInProgress, Component); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 9833ef481af70..62d51046b3c46 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -174,7 +174,11 @@ import { prepareToReadContext, scheduleWorkOnParentPath, } from './ReactFiberNewContext.old'; -import {renderWithHooks, bailoutHooks} from './ReactFiberHooks.old'; +import { + renderWithHooks, + checkDidRenderIdHook, + bailoutHooks, +} from './ReactFiberHooks.old'; import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.old'; import { getMaskedContext, @@ -240,6 +244,7 @@ import { getForksAtLevel, isForkedChild, pushTreeId, + pushMaterializedTreeId, } from './ReactFiberTreeContext.old'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -365,6 +370,7 @@ function updateForwardRef( // The rest is a fork of updateFunctionComponent let nextChildren; + let hasId; prepareToReadContext(workInProgress, renderLanes); if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -380,6 +386,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictLegacyMode @@ -394,6 +401,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } @@ -408,6 +416,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -418,6 +427,10 @@ function updateForwardRef( return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } + if (getIsHydrating() && hasId) { + pushMaterializedTreeId(workInProgress); + } + // React DevTools reads this flag. workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren, renderLanes); @@ -970,6 +983,7 @@ function updateFunctionComponent( } let nextChildren; + let hasId; prepareToReadContext(workInProgress, renderLanes); if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -985,6 +999,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictLegacyMode @@ -999,6 +1014,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } @@ -1013,6 +1029,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -1023,6 +1040,10 @@ function updateFunctionComponent( return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } + if (getIsHydrating() && hasId) { + pushMaterializedTreeId(workInProgress); + } + // React DevTools reads this flag. workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren, renderLanes); @@ -1593,6 +1614,7 @@ function mountIndeterminateComponent( prepareToReadContext(workInProgress, renderLanes); let value; + let hasId; if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -1629,6 +1651,7 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); setIsRendering(false); } else { value = renderWithHooks( @@ -1639,6 +1662,7 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -1758,12 +1782,17 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } } } + if (getIsHydrating() && hasId) { + pushMaterializedTreeId(workInProgress); + } + reconcileChildren(null, workInProgress, value, renderLanes); if (__DEV__) { validateFunctionComponentInDev(workInProgress, Component); diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 1238d673725ac..2ea341864642c 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -110,7 +110,7 @@ import { } from './ReactUpdateQueue.new'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; -import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.new'; +import {getTreeId} from './ReactFiberTreeContext.new'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -432,6 +432,7 @@ export function renderWithHooks( let numberOfReRenders: number = 0; do { didScheduleRenderPhaseUpdateDuringThisPass = false; + localIdCounter = 0; if (numberOfReRenders >= RE_RENDER_LIMIT) { throw new Error( @@ -513,6 +514,8 @@ export function renderWithHooks( } didScheduleRenderPhaseUpdate = false; + // This is reset by checkDidRenderIdHook + // localIdCounter = 0; if (didRenderTooFewHooks) { throw new Error( @@ -541,25 +544,18 @@ export function renderWithHooks( } } } - - if (localIdCounter !== 0) { - localIdCounter = 0; - if (getIsHydrating()) { - // This component materialized an id. This will affect any ids that appear - // in its children. - const returnFiber = workInProgress.return; - if (returnFiber !== null) { - const numberOfForks = 1; - const slotIndex = 0; - pushTreeFork(workInProgress, numberOfForks); - pushTreeId(workInProgress, numberOfForks, slotIndex); - } - } - } - return children; } +export function checkDidRenderIdHook() { + // This should be called immediately after every renderWithHooks call. + // Conceptually, it's part of the return value of renderWithHooks; it's only a + // separate function to avoid using an array tuple. + const didRenderIdHook = localIdCounter !== 0; + localIdCounter = 0; + return didRenderIdHook; +} + export function bailoutHooks( current: Fiber, workInProgress: Fiber, diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 3b0b87cb1ab7c..ff6a9f652fb4d 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -110,7 +110,7 @@ import { } from './ReactUpdateQueue.old'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; -import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.old'; +import {getTreeId} from './ReactFiberTreeContext.old'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -432,6 +432,7 @@ export function renderWithHooks( let numberOfReRenders: number = 0; do { didScheduleRenderPhaseUpdateDuringThisPass = false; + localIdCounter = 0; if (numberOfReRenders >= RE_RENDER_LIMIT) { throw new Error( @@ -513,6 +514,8 @@ export function renderWithHooks( } didScheduleRenderPhaseUpdate = false; + // This is reset by checkDidRenderIdHook + // localIdCounter = 0; if (didRenderTooFewHooks) { throw new Error( @@ -541,25 +544,18 @@ export function renderWithHooks( } } } - - if (localIdCounter !== 0) { - localIdCounter = 0; - if (getIsHydrating()) { - // This component materialized an id. This will affect any ids that appear - // in its children. - const returnFiber = workInProgress.return; - if (returnFiber !== null) { - const numberOfForks = 1; - const slotIndex = 0; - pushTreeFork(workInProgress, numberOfForks); - pushTreeId(workInProgress, numberOfForks, slotIndex); - } - } - } - return children; } +export function checkDidRenderIdHook() { + // This should be called immediately after every renderWithHooks call. + // Conceptually, it's part of the return value of renderWithHooks; it's only a + // separate function to avoid using an array tuple. + const didRenderIdHook = localIdCounter !== 0; + localIdCounter = 0; + return didRenderIdHook; +} + export function bailoutHooks( current: Fiber, workInProgress: Fiber, diff --git a/packages/react-reconciler/src/ReactFiberTreeContext.new.js b/packages/react-reconciler/src/ReactFiberTreeContext.new.js index 0725ba577e647..968e66155d3fa 100644 --- a/packages/react-reconciler/src/ReactFiberTreeContext.new.js +++ b/packages/react-reconciler/src/ReactFiberTreeContext.new.js @@ -201,6 +201,20 @@ export function pushTreeId( } } +export function pushMaterializedTreeId(workInProgress: Fiber) { + warnIfNotHydrating(); + + // This component materialized an id. This will affect any ids that appear + // in its children. + const returnFiber = workInProgress.return; + if (returnFiber !== null) { + const numberOfForks = 1; + const slotIndex = 0; + pushTreeFork(workInProgress, numberOfForks); + pushTreeId(workInProgress, numberOfForks, slotIndex); + } +} + function getBitLength(number: number): number { return 32 - clz32(number); } diff --git a/packages/react-reconciler/src/ReactFiberTreeContext.old.js b/packages/react-reconciler/src/ReactFiberTreeContext.old.js index a4ba3c3ddb931..82990b22f983d 100644 --- a/packages/react-reconciler/src/ReactFiberTreeContext.old.js +++ b/packages/react-reconciler/src/ReactFiberTreeContext.old.js @@ -201,6 +201,20 @@ export function pushTreeId( } } +export function pushMaterializedTreeId(workInProgress: Fiber) { + warnIfNotHydrating(); + + // This component materialized an id. This will affect any ids that appear + // in its children. + const returnFiber = workInProgress.return; + if (returnFiber !== null) { + const numberOfForks = 1; + const slotIndex = 0; + pushTreeFork(workInProgress, numberOfForks); + pushTreeId(workInProgress, numberOfForks, slotIndex); + } +} + function getBitLength(number: number): number { return 32 - clz32(number); } diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 7b31e6b37ed9e..926a1969bb48a 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -199,6 +199,7 @@ export function finishHooks( // work-in-progress hooks and applying the additional updates on top. Keep // restarting until no more updates are scheduled. didScheduleRenderPhaseUpdate = false; + localIdCounter = 0; numberOfReRenders += 1; // Start over from the beginning of the list From 255221c86930e31c144713eb466bf0a00ab8fd7e Mon Sep 17 00:00:00 2001 From: EzzAk Date: Wed, 3 Nov 2021 08:27:30 -0700 Subject: [PATCH 080/109] [DevTools] Add open in editor for fb (#22649) Co-authored-by: Brian Vaughn --- .../react-devtools-core/webpack.standalone.js | 2 + .../webpack.config.js | 2 + .../react-devtools-inline/webpack.config.js | 3 ++ .../react-devtools-shared/src/constants.js | 3 ++ .../src/devtools/views/ButtonIcon.js | 8 ++++ .../views/Components/InspectedElement.js | 40 ++++++++++++++++++- .../views/Settings/ComponentsSettings.js | 22 +++++++++- .../views/Settings/SettingsShared.css | 8 ++++ .../src/devtools/views/hooks.js | 3 ++ packages/react-devtools-shared/src/utils.js | 17 ++++++++ .../react-devtools-shell/webpack.config.js | 3 ++ 11 files changed, 108 insertions(+), 3 deletions(-) diff --git a/packages/react-devtools-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js index 9d7c1fc34a685..234d638353f43 100644 --- a/packages/react-devtools-core/webpack.standalone.js +++ b/packages/react-devtools-core/webpack.standalone.js @@ -30,6 +30,7 @@ const __DEV__ = NODE_ENV === 'development'; const DEVTOOLS_VERSION = getVersionString(); +const EDITOR_URL = process.env.EDITOR_URL || null; const LOGGING_URL = process.env.LOGGING_URL || null; const featureFlagTarget = @@ -83,6 +84,7 @@ module.exports = { __TEST__: NODE_ENV === 'test', 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.LOGGING_URL': `"${LOGGING_URL}"`, 'process.env.NODE_ENV': `"${NODE_ENV}"`, diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index 99b1d18d3a999..ac7c51f3bdc5e 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -32,6 +32,7 @@ const __DEV__ = NODE_ENV === 'development'; const DEVTOOLS_VERSION = getVersionString(process.env.DEVTOOLS_VERSION); +const EDITOR_URL = process.env.EDITOR_URL || null; const LOGGING_URL = process.env.LOGGING_URL || null; const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss'; @@ -92,6 +93,7 @@ module.exports = { __TEST__: NODE_ENV === 'test', 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.LOGGING_URL': `"${LOGGING_URL}"`, 'process.env.NODE_ENV': `"${NODE_ENV}"`, diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 7c2325bf70f45..ab7bcb8be3650 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -20,6 +20,8 @@ if (!NODE_ENV) { const __DEV__ = NODE_ENV === 'development'; +const EDITOR_URL = process.env.EDITOR_URL || null; + const DEVTOOLS_VERSION = getVersionString(); const babelOptions = { @@ -76,6 +78,7 @@ module.exports = { __TEST__: NODE_ENV === 'test', 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.NODE_ENV': `"${NODE_ENV}"`, 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`, diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 3c3aaae2fc461..798bdb9d900e3 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -32,6 +32,9 @@ export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY = export const SESSION_STORAGE_LAST_SELECTION_KEY = 'React::DevTools::lastSelection'; +export const LOCAL_STORAGE_OPEN_IN_EDITOR_URL = + 'React::DevTools::openInEditorUrl'; + export const LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY = 'React::DevTools::parseHookNames'; diff --git a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js index 8239a765f90b6..b384018671d92 100644 --- a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js +++ b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js @@ -19,6 +19,7 @@ export type IconType = | 'copy' | 'delete' | 'down' + | 'editor' | 'expanded' | 'export' | 'filter' @@ -72,6 +73,9 @@ export default function ButtonIcon({className = '', type}: Props) { case 'down': pathData = PATH_DOWN; break; + case 'editor': + pathData = PATH_EDITOR; + break; case 'expanded': pathData = PATH_EXPANDED; break; @@ -268,3 +272,7 @@ const PATH_VIEW_DOM = ` const PATH_VIEW_SOURCE = ` M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z `; + +const PATH_EDITOR = ` + M7 5h10v2h2V3c0-1.1-.9-1.99-2-1.99L7 1c-1.1 0-2 .9-2 2v4h2V5zm8.41 11.59L20 12l-4.59-4.59L14 8.83 17.17 12 14 15.17l1.41 1.42zM10 15.17L6.83 12 10 8.83 8.59 7.41 4 12l4.59 4.59L10 15.17zM17 19H7v-2H5v4c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-4h-2v2z +`; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js index f63da7b54a67d..9ebd30a6ec0b3 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js @@ -8,7 +8,7 @@ */ import * as React from 'react'; -import {useCallback, useContext} from 'react'; +import {useCallback, useContext, useSyncExternalStore} from 'react'; import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; import {BridgeContext, StoreContext, OptionsContext} from '../context'; import Button from '../Button'; @@ -20,6 +20,8 @@ import {ElementTypeSuspense} from 'react-devtools-shared/src/types'; import CannotSuspendWarningMessage from './CannotSuspendWarningMessage'; import InspectedElementView from './InspectedElementView'; import {InspectedElementContext} from './InspectedElementContext'; +import {getOpenInEditorURL} from '../../../utils'; +import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants'; import styles from './InspectedElement.css'; @@ -123,6 +125,21 @@ export default function InspectedElementWrapper(_: Props) { inspectedElement != null && inspectedElement.canToggleSuspense; + const editorURL = useSyncExternalStore( + function subscribe(callback) { + window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback); + return function unsubscribe() { + window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback); + }; + }, + function getState() { + return getOpenInEditorURL(); + }, + ); + + const canOpenInEditor = + editorURL && inspectedElement != null && inspectedElement.source != null; + const toggleErrored = useCallback(() => { if (inspectedElement == null || targetErrorBoundaryID == null) { return; @@ -198,6 +215,18 @@ export default function InspectedElementWrapper(_: Props) { } }, [bridge, dispatch, element, isSuspended, modalDialogDispatch, store]); + const onOpenInEditor = useCallback(() => { + const source = inspectedElement?.source; + if (source == null || editorURL == null) { + return; + } + + const url = new URL(editorURL); + url.href = url.href.replace('{path}', source.fileName); + url.href = url.href.replace('{line}', String(source.lineNumber)); + window.open(url); + }, [inspectedElement, editorURL]); + if (element === null) { return (
    @@ -223,7 +252,14 @@ export default function InspectedElementWrapper(_: Props) { {element.displayName}
    - + {canOpenInEditor && ( + + )} {canToggleError && ( ( + LOCAL_STORAGE_OPEN_IN_EDITOR_URL, + getDefaultOpenInEditorURL(), + ); + const [componentFilters, setComponentFilters] = useState< Array, >(() => [...store.componentFilters]); @@ -271,6 +278,19 @@ export default function ComponentsSettings(_: {||}) { (may be slow) + +
    Hide components where...
    diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css index 3ddaf13907702..9c0ee028c9968 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css @@ -14,6 +14,10 @@ margin-bottom: 0; } +.OpenInURLSetting { + margin: 0.5rem 0; +} + .OptionGroup { display: inline-flex; flex-direction: row; @@ -30,6 +34,10 @@ margin-right: 0.5rem; } +.Spacer { + height: 0.5rem; +} + .Select { } diff --git a/packages/react-devtools-shared/src/devtools/views/hooks.js b/packages/react-devtools-shared/src/devtools/views/hooks.js index 76dad56774540..1a34373fc7e19 100644 --- a/packages/react-devtools-shared/src/devtools/views/hooks.js +++ b/packages/react-devtools-shared/src/devtools/views/hooks.js @@ -170,6 +170,9 @@ export function useLocalStorage( value instanceof Function ? (value: any)(storedValue) : value; setStoredValue(valueToStore); localStorageSetItem(key, JSON.stringify(valueToStore)); + + // Notify listeners that this setting has changed. + window.dispatchEvent(new Event(key)); } catch (error) { console.log(error); } diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 84c977f19d4e7..0c50374ae30b6 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -34,6 +34,7 @@ import { import {ElementTypeRoot} from 'react-devtools-shared/src/types'; import { LOCAL_STORAGE_FILTER_PREFERENCES_KEY, + LOCAL_STORAGE_OPEN_IN_EDITOR_URL, LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS, LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY, @@ -386,6 +387,22 @@ export function setShowInlineWarningsAndErrors(value: boolean): void { ); } +export function getDefaultOpenInEditorURL(): string { + return typeof process.env.EDITOR_URL === 'string' + ? process.env.EDITOR_URL + : ''; +} + +export function getOpenInEditorURL(): string { + try { + const raw = localStorageGetItem(LOCAL_STORAGE_OPEN_IN_EDITOR_URL); + if (raw != null) { + return JSON.parse(raw); + } + } catch (error) {} + return getDefaultOpenInEditorURL(); +} + export function separateDisplayNameAndHOCs( displayName: string | null, type: ElementType, diff --git a/packages/react-devtools-shell/webpack.config.js b/packages/react-devtools-shell/webpack.config.js index 19f53f5d08b09..18c55d1d8b740 100644 --- a/packages/react-devtools-shell/webpack.config.js +++ b/packages/react-devtools-shell/webpack.config.js @@ -24,6 +24,8 @@ if (!TARGET) { process.exit(1); } +const EDITOR_URL = process.env.EDITOR_URL || null; + const builtModulesDir = resolve( __dirname, '..', @@ -69,6 +71,7 @@ const config = { __PROFILE__: false, __TEST__: NODE_ENV === 'test', 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-shell"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`, From 51c558aeb6f5b0ae639d975fd4044007a73115c1 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 3 Nov 2021 15:10:29 -0400 Subject: [PATCH 081/109] Rename (some) "scheduling profiler" references to "timeline" (#22690) --- .../react-devtools-extensions/src/main.js | 2 +- .../README.md | 2 +- .../src/content-views/constants.js | 86 +++++----- .../src/import-worker/preprocessData.js | 2 +- .../src/backend/types.js | 2 +- .../react-devtools-shared/src/constants.js | 160 +++++++++--------- .../src/devtools/views/ErrorBoundary/cache.js | 2 +- .../src/devtools/views/Icon.js | 4 +- .../Profiler/ClearProfilingDataButton.js | 4 +- .../src/devtools/views/Profiler/Profiler.js | 20 +-- .../views/Profiler/ProfilerContext.js | 4 +- .../Profiler/ProfilingImportExportButtons.js | 4 +- .../src/dynamicImportCache.js | 2 +- .../src/hookNamesCache.js | 2 +- .../src/inspectedElementCache.js | 2 +- .../src/ReactFiberLane.new.js | 2 +- .../src/ReactFiberLane.old.js | 2 +- .../src/SchedulingProfiler.js | 20 +-- packages/shared/ReactFeatureFlags.js | 2 +- scripts/rollup/wrappers.js | 2 +- 20 files changed, 158 insertions(+), 168 deletions(-) diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index 70aaf92a7d528..36625cf74e30f 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -152,7 +152,7 @@ function createPanelIfReactLoaded() { isProfiling, supportsReloadAndProfile: isChrome || isEdge, supportsProfiling, - // At this time, the scheduling profiler can only parse Chrome performance profiles. + // At this time, the timeline can only parse Chrome performance profiles. supportsSchedulingProfiler: isChrome, supportsTraceUpdates: true, }); diff --git a/packages/react-devtools-scheduling-profiler/README.md b/packages/react-devtools-scheduling-profiler/README.md index 457bec25efcf2..2b80944de281b 100644 --- a/packages/react-devtools-scheduling-profiler/README.md +++ b/packages/react-devtools-scheduling-profiler/README.md @@ -1,3 +1,3 @@ # React Concurrent Mode Profiler -This package contains the new/experimental "scheduling profiler" for React 18. This profiler exists as its own project because it was initially deployed as a standalone app. It has since been moved into the DevTools Profiler under the "Scheduling" tab. This package will likely eventually be moved into `react-devtools-shared`. \ No newline at end of file +This package contains the new/experimental "timeline" for React 18. This profiler exists as its own project because it was initially deployed as a standalone app. It has since been moved into the DevTools Profiler under the "Scheduling" tab. This package will likely eventually be moved into `react-devtools-shared`. \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js index d0895d2aff8e7..216234508f5e1 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -109,86 +109,82 @@ export function updateColorsToMatchTheme(element: Element): boolean { COLORS = { BACKGROUND: computedStyle.getPropertyValue('--color-background'), INTERNAL_MODULE_FRAME: computedStyle.getPropertyValue( - '--color-scheduling-profiler-internal-module', + '--color-timeline-internal-module', ), INTERNAL_MODULE_FRAME_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-internal-module-hover', + '--color-timeline-internal-module-hover', ), INTERNAL_MODULE_FRAME_TEXT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-internal-module-text', + '--color-timeline-internal-module-text', ), NATIVE_EVENT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-native-event', + '--color-timeline-native-event', ), NATIVE_EVENT_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-native-event-hover', + '--color-timeline-native-event-hover', ), NETWORK_PRIMARY: computedStyle.getPropertyValue( - '--color-scheduling-profiler-network-primary', + '--color-timeline-network-primary', ), NETWORK_PRIMARY_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-network-primary-hover', + '--color-timeline-network-primary-hover', ), NETWORK_SECONDARY: computedStyle.getPropertyValue( - '--color-scheduling-profiler-network-secondary', + '--color-timeline-network-secondary', ), NETWORK_SECONDARY_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-network-secondary-hover', + '--color-timeline-network-secondary-hover', ), PRIORITY_BACKGROUND: computedStyle.getPropertyValue( - '--color-scheduling-profiler-priority-background', + '--color-timeline-priority-background', ), PRIORITY_BORDER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-priority-border', + '--color-timeline-priority-border', ), PRIORITY_LABEL: computedStyle.getPropertyValue('--color-text'), - USER_TIMING: computedStyle.getPropertyValue( - '--color-scheduling-profiler-user-timing', - ), + USER_TIMING: computedStyle.getPropertyValue('--color-timeline-user-timing'), USER_TIMING_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-user-timing-hover', - ), - REACT_IDLE: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-idle', + '--color-timeline-user-timing-hover', ), + REACT_IDLE: computedStyle.getPropertyValue('--color-timeline-react-idle'), REACT_IDLE_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-idle-hover', + '--color-timeline-react-idle-hover', ), REACT_RENDER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-render', + '--color-timeline-react-render', ), REACT_RENDER_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-render-hover', + '--color-timeline-react-render-hover', ), REACT_RENDER_TEXT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-render-text', + '--color-timeline-react-render-text', ), REACT_COMMIT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-commit', + '--color-timeline-react-commit', ), REACT_COMMIT_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-commit-hover', + '--color-timeline-react-commit-hover', ), REACT_COMMIT_TEXT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-commit-text', + '--color-timeline-react-commit-text', ), REACT_LAYOUT_EFFECTS: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-layout-effects', + '--color-timeline-react-layout-effects', ), REACT_LAYOUT_EFFECTS_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-layout-effects-hover', + '--color-timeline-react-layout-effects-hover', ), REACT_LAYOUT_EFFECTS_TEXT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-layout-effects-text', + '--color-timeline-react-layout-effects-text', ), REACT_PASSIVE_EFFECTS: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-passive-effects', + '--color-timeline-react-passive-effects', ), REACT_PASSIVE_EFFECTS_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-passive-effects-hover', + '--color-timeline-react-passive-effects-hover', ), REACT_PASSIVE_EFFECTS_TEXT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-passive-effects-text', + '--color-timeline-react-passive-effects-text', ), REACT_RESIZE_BAR: computedStyle.getPropertyValue('--color-resize-bar'), REACT_RESIZE_BAR_ACTIVE: computedStyle.getPropertyValue( @@ -201,44 +197,42 @@ export function updateColorsToMatchTheme(element: Element): boolean { '--color-resize-bar-dot', ), REACT_SCHEDULE: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-schedule', + '--color-timeline-react-schedule', ), REACT_SCHEDULE_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-schedule-hover', + '--color-timeline-react-schedule-hover', ), REACT_SUSPENSE_REJECTED_EVENT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspense-rejected', + '--color-timeline-react-suspense-rejected', ), REACT_SUSPENSE_REJECTED_EVENT_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspense-rejected-hover', + '--color-timeline-react-suspense-rejected-hover', ), REACT_SUSPENSE_RESOLVED_EVENT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspense-resolved', + '--color-timeline-react-suspense-resolved', ), REACT_SUSPENSE_RESOLVED_EVENT_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspense-resolved-hover', + '--color-timeline-react-suspense-resolved-hover', ), REACT_SUSPENSE_UNRESOLVED_EVENT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspense-unresolved', + '--color-timeline-react-suspense-unresolved', ), REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspense-unresolved-hover', + '--color-timeline-react-suspense-unresolved-hover', ), REACT_THROWN_ERROR: computedStyle.getPropertyValue( - '--color-scheduling-profiler-thrown-error', + '--color-timeline-thrown-error', ), REACT_THROWN_ERROR_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-thrown-error-hover', + '--color-timeline-thrown-error-hover', ), REACT_WORK_BORDER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-work-border', + '--color-timeline-react-work-border', ), SCROLL_CARET: computedStyle.getPropertyValue('--color-scroll-caret'), - TEXT_COLOR: computedStyle.getPropertyValue( - '--color-scheduling-profiler-text-color', - ), + TEXT_COLOR: computedStyle.getPropertyValue('--color-timeline-text-color'), TEXT_DIM_COLOR: computedStyle.getPropertyValue( - '--color-scheduling-profiler-text-dim-color', + '--color-timeline-text-dim-color', ), TIME_MARKER_LABEL: computedStyle.getPropertyValue('--color-text'), WARNING_BACKGROUND: computedStyle.getPropertyValue( diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js index d6c911af1d076..caf27e707563b 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -575,7 +575,7 @@ function processTimelineEvent( } } - // TODO (scheduling profiler) Maybe we should calculate depth in post, + // TODO (timeline) Maybe we should calculate depth in post, // so unresolved Suspense requests don't take up space. // We can't know if they'll be resolved or not at this point. // We'll just give them a default (fake) duration width. diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 46e1287797981..333ee30914cdd 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -420,7 +420,7 @@ export type DevToolsHook = { didError?: boolean, ) => void, - // Scheduling Profiler internal module filtering + // Timeline internal module filtering getInternalModuleRanges: () => Array<[string, string]>, registerInternalModuleStart: (moduleStartError: Error) => void, registerInternalModuleStop: (moduleStopError: Error) => void, diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 798bdb9d900e3..8898c27787f40 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -154,46 +154,46 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-resize-bar-active': '#dcdcdc', '--color-resize-bar-border': '#d1d1d1', '--color-resize-bar-dot': '#333333', - '--color-scheduling-profiler-internal-module': '#d1d1d1', - '--color-scheduling-profiler-internal-module-hover': '#c9c9c9', - '--color-scheduling-profiler-internal-module-text': '#444', - '--color-scheduling-profiler-native-event': '#ccc', - '--color-scheduling-profiler-native-event-hover': '#aaa', - '--color-scheduling-profiler-network-primary': '#fcf3dc', - '--color-scheduling-profiler-network-primary-hover': '#f0e7d1', - '--color-scheduling-profiler-network-secondary': '#efc457', - '--color-scheduling-profiler-network-secondary-hover': '#e3ba52', - '--color-scheduling-profiler-priority-background': '#f6f6f6', - '--color-scheduling-profiler-priority-border': '#eeeeee', - '--color-scheduling-profiler-user-timing': '#c9cacd', - '--color-scheduling-profiler-user-timing-hover': '#93959a', - '--color-scheduling-profiler-react-idle': '#d3e5f6', - '--color-scheduling-profiler-react-idle-hover': '#c3d9ef', - '--color-scheduling-profiler-react-render': '#9fc3f3', - '--color-scheduling-profiler-react-render-hover': '#83afe9', - '--color-scheduling-profiler-react-render-text': '#11365e', - '--color-scheduling-profiler-react-commit': '#c88ff0', - '--color-scheduling-profiler-react-commit-hover': '#b281d6', - '--color-scheduling-profiler-react-commit-text': '#3e2c4a', - '--color-scheduling-profiler-react-layout-effects': '#b281d6', - '--color-scheduling-profiler-react-layout-effects-hover': '#9d71bd', - '--color-scheduling-profiler-react-layout-effects-text': '#3e2c4a', - '--color-scheduling-profiler-react-passive-effects': '#b281d6', - '--color-scheduling-profiler-react-passive-effects-hover': '#9d71bd', - '--color-scheduling-profiler-react-passive-effects-text': '#3e2c4a', - '--color-scheduling-profiler-react-schedule': '#9fc3f3', - '--color-scheduling-profiler-react-schedule-hover': '#2683E2', - '--color-scheduling-profiler-react-suspense-rejected': '#f1cc14', - '--color-scheduling-profiler-react-suspense-rejected-hover': '#ffdf37', - '--color-scheduling-profiler-react-suspense-resolved': '#a6e59f', - '--color-scheduling-profiler-react-suspense-resolved-hover': '#89d281', - '--color-scheduling-profiler-react-suspense-unresolved': '#c9cacd', - '--color-scheduling-profiler-react-suspense-unresolved-hover': '#93959a', - '--color-scheduling-profiler-thrown-error': '#ee1638', - '--color-scheduling-profiler-thrown-error-hover': '#da1030', - '--color-scheduling-profiler-text-color': '#000000', - '--color-scheduling-profiler-text-dim-color': '#ccc', - '--color-scheduling-profiler-react-work-border': '#eeeeee', + '--color-timeline-internal-module': '#d1d1d1', + '--color-timeline-internal-module-hover': '#c9c9c9', + '--color-timeline-internal-module-text': '#444', + '--color-timeline-native-event': '#ccc', + '--color-timeline-native-event-hover': '#aaa', + '--color-timeline-network-primary': '#fcf3dc', + '--color-timeline-network-primary-hover': '#f0e7d1', + '--color-timeline-network-secondary': '#efc457', + '--color-timeline-network-secondary-hover': '#e3ba52', + '--color-timeline-priority-background': '#f6f6f6', + '--color-timeline-priority-border': '#eeeeee', + '--color-timeline-user-timing': '#c9cacd', + '--color-timeline-user-timing-hover': '#93959a', + '--color-timeline-react-idle': '#d3e5f6', + '--color-timeline-react-idle-hover': '#c3d9ef', + '--color-timeline-react-render': '#9fc3f3', + '--color-timeline-react-render-hover': '#83afe9', + '--color-timeline-react-render-text': '#11365e', + '--color-timeline-react-commit': '#c88ff0', + '--color-timeline-react-commit-hover': '#b281d6', + '--color-timeline-react-commit-text': '#3e2c4a', + '--color-timeline-react-layout-effects': '#b281d6', + '--color-timeline-react-layout-effects-hover': '#9d71bd', + '--color-timeline-react-layout-effects-text': '#3e2c4a', + '--color-timeline-react-passive-effects': '#b281d6', + '--color-timeline-react-passive-effects-hover': '#9d71bd', + '--color-timeline-react-passive-effects-text': '#3e2c4a', + '--color-timeline-react-schedule': '#9fc3f3', + '--color-timeline-react-schedule-hover': '#2683E2', + '--color-timeline-react-suspense-rejected': '#f1cc14', + '--color-timeline-react-suspense-rejected-hover': '#ffdf37', + '--color-timeline-react-suspense-resolved': '#a6e59f', + '--color-timeline-react-suspense-resolved-hover': '#89d281', + '--color-timeline-react-suspense-unresolved': '#c9cacd', + '--color-timeline-react-suspense-unresolved-hover': '#93959a', + '--color-timeline-thrown-error': '#ee1638', + '--color-timeline-thrown-error-hover': '#da1030', + '--color-timeline-text-color': '#000000', + '--color-timeline-text-dim-color': '#ccc', + '--color-timeline-react-work-border': '#eeeeee', '--color-search-match': 'yellow', '--color-search-match-current': '#f7923b', '--color-selected-tree-highlight-active': 'rgba(0, 136, 250, 0.1)', @@ -298,46 +298,46 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-resize-bar-active': '#31363f', '--color-resize-bar-border': '#3d424a', '--color-resize-bar-dot': '#cfd1d5', - '--color-scheduling-profiler-internal-module': '#303542', - '--color-scheduling-profiler-internal-module-hover': '#363b4a', - '--color-scheduling-profiler-internal-module-text': '#7f8899', - '--color-scheduling-profiler-native-event': '#b2b2b2', - '--color-scheduling-profiler-native-event-hover': '#949494', - '--color-scheduling-profiler-network-primary': '#fcf3dc', - '--color-scheduling-profiler-network-primary-hover': '#e3dbc5', - '--color-scheduling-profiler-network-secondary': '#efc457', - '--color-scheduling-profiler-network-secondary-hover': '#d6af4d', - '--color-scheduling-profiler-priority-background': '#1d2129', - '--color-scheduling-profiler-priority-border': '#282c34', - '--color-scheduling-profiler-user-timing': '#c9cacd', - '--color-scheduling-profiler-user-timing-hover': '#93959a', - '--color-scheduling-profiler-react-idle': '#3d485b', - '--color-scheduling-profiler-react-idle-hover': '#465269', - '--color-scheduling-profiler-react-render': '#2683E2', - '--color-scheduling-profiler-react-render-hover': '#1a76d4', - '--color-scheduling-profiler-react-render-text': '#11365e', - '--color-scheduling-profiler-react-commit': '#731fad', - '--color-scheduling-profiler-react-commit-hover': '#611b94', - '--color-scheduling-profiler-react-commit-text': '#e5c1ff', - '--color-scheduling-profiler-react-layout-effects': '#611b94', - '--color-scheduling-profiler-react-layout-effects-hover': '#51167a', - '--color-scheduling-profiler-react-layout-effects-text': '#e5c1ff', - '--color-scheduling-profiler-react-passive-effects': '#611b94', - '--color-scheduling-profiler-react-passive-effects-hover': '#51167a', - '--color-scheduling-profiler-react-passive-effects-text': '#e5c1ff', - '--color-scheduling-profiler-react-schedule': '#2683E2', - '--color-scheduling-profiler-react-schedule-hover': '#1a76d4', - '--color-scheduling-profiler-react-suspense-rejected': '#f1cc14', - '--color-scheduling-profiler-react-suspense-rejected-hover': '#e4c00f', - '--color-scheduling-profiler-react-suspense-resolved': '#a6e59f', - '--color-scheduling-profiler-react-suspense-resolved-hover': '#89d281', - '--color-scheduling-profiler-react-suspense-unresolved': '#c9cacd', - '--color-scheduling-profiler-react-suspense-unresolved-hover': '#93959a', - '--color-scheduling-profiler-thrown-error': '#fb3655', - '--color-scheduling-profiler-thrown-error-hover': '#f82042', - '--color-scheduling-profiler-text-color': '#282c34', - '--color-scheduling-profiler-text-dim-color': '#555b66', - '--color-scheduling-profiler-react-work-border': '#3d424a', + '--color-timeline-internal-module': '#303542', + '--color-timeline-internal-module-hover': '#363b4a', + '--color-timeline-internal-module-text': '#7f8899', + '--color-timeline-native-event': '#b2b2b2', + '--color-timeline-native-event-hover': '#949494', + '--color-timeline-network-primary': '#fcf3dc', + '--color-timeline-network-primary-hover': '#e3dbc5', + '--color-timeline-network-secondary': '#efc457', + '--color-timeline-network-secondary-hover': '#d6af4d', + '--color-timeline-priority-background': '#1d2129', + '--color-timeline-priority-border': '#282c34', + '--color-timeline-user-timing': '#c9cacd', + '--color-timeline-user-timing-hover': '#93959a', + '--color-timeline-react-idle': '#3d485b', + '--color-timeline-react-idle-hover': '#465269', + '--color-timeline-react-render': '#2683E2', + '--color-timeline-react-render-hover': '#1a76d4', + '--color-timeline-react-render-text': '#11365e', + '--color-timeline-react-commit': '#731fad', + '--color-timeline-react-commit-hover': '#611b94', + '--color-timeline-react-commit-text': '#e5c1ff', + '--color-timeline-react-layout-effects': '#611b94', + '--color-timeline-react-layout-effects-hover': '#51167a', + '--color-timeline-react-layout-effects-text': '#e5c1ff', + '--color-timeline-react-passive-effects': '#611b94', + '--color-timeline-react-passive-effects-hover': '#51167a', + '--color-timeline-react-passive-effects-text': '#e5c1ff', + '--color-timeline-react-schedule': '#2683E2', + '--color-timeline-react-schedule-hover': '#1a76d4', + '--color-timeline-react-suspense-rejected': '#f1cc14', + '--color-timeline-react-suspense-rejected-hover': '#e4c00f', + '--color-timeline-react-suspense-resolved': '#a6e59f', + '--color-timeline-react-suspense-resolved-hover': '#89d281', + '--color-timeline-react-suspense-unresolved': '#c9cacd', + '--color-timeline-react-suspense-unresolved-hover': '#93959a', + '--color-timeline-thrown-error': '#fb3655', + '--color-timeline-thrown-error-hover': '#f82042', + '--color-timeline-text-color': '#282c34', + '--color-timeline-text-dim-color': '#555b66', + '--color-timeline-react-work-border': '#3d424a', '--color-search-match': 'yellow', '--color-search-match-current': '#f7923b', '--color-selected-tree-highlight-active': 'rgba(23, 143, 185, 0.15)', diff --git a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js index 9e5061cb804ee..047c0788c3493 100644 --- a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js +++ b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js @@ -71,7 +71,7 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null { callbacks.add(callback); }, - // Optional property used by Scheduling Profiler: + // Optional property used by Timeline: displayName: `Searching GitHub issues for error "${errorMessage}"`, }; const wake = () => { diff --git a/packages/react-devtools-shared/src/devtools/views/Icon.js b/packages/react-devtools-shared/src/devtools/views/Icon.js index c9ae931f5ee74..cec6ae34ef910 100644 --- a/packages/react-devtools-shared/src/devtools/views/Icon.js +++ b/packages/react-devtools-shared/src/devtools/views/Icon.js @@ -21,7 +21,7 @@ export type IconType = | 'flame-chart' | 'profiler' | 'ranked-chart' - | 'scheduling-profiler' + | 'timeline' | 'search' | 'settings' | 'store-as-global-variable' @@ -65,7 +65,7 @@ export default function Icon({className = '', type}: Props) { case 'ranked-chart': pathData = PATH_RANKED_CHART; break; - case 'scheduling-profiler': + case 'timeline': pathData = PATH_SCHEDULING_PROFILER; break; case 'search': diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js index a6abd38712f0e..a978277ba8697 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js @@ -26,14 +26,14 @@ export default function ClearProfilingDataButton() { const {profilerStore} = store; let doesHaveData = false; - if (selectedTabID === 'scheduling-profiler') { + if (selectedTabID === 'timeline') { doesHaveData = schedulingProfilerData !== null; } else { doesHaveData = didRecordCommits; } const clear = () => { - if (selectedTabID === 'scheduling-profiler') { + if (selectedTabID === 'timeline') { clearSchedulingProfilerData(); } else { profilerStore.clear(); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index 2973fbf395859..49107506e48f6 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -48,7 +48,7 @@ function Profiler(_: {||}) { let isLegacyProfilerSelected = false; let view = null; - if (didRecordCommits || selectedTabID === 'scheduling-profiler') { + if (didRecordCommits || selectedTabID === 'timeline') { switch (selectedTabID) { case 'flame-chart': isLegacyProfilerSelected = true; @@ -58,7 +58,7 @@ function Profiler(_: {||}) { isLegacyProfilerSelected = true; view = ; break; - case 'scheduling-profiler': + case 'timeline': view = ; break; default: @@ -104,14 +104,10 @@ function Profiler(_: {||}) {
    @@ -168,10 +164,10 @@ const tabsWithSchedulingProfiler = [ ...tabs, null, // Divider/separator { - id: 'scheduling-profiler', - icon: 'scheduling-profiler', - label: 'Scheduling', - title: 'Scheduling Profiler', + id: 'timeline', + icon: 'timeline', + label: 'Timeline', + title: 'Timeline', }, ]; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js index d5e43dc6a3d2a..6e486091e1419 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js @@ -19,8 +19,8 @@ import {StoreContext} from '../context'; import type {ProfilingDataFrontend} from './types'; -// TODO (scheduling profiler) Should this be its own context? -export type TabID = 'flame-chart' | 'ranked-chart' | 'scheduling-profiler'; +// TODO (timeline) Should this be its own context? +export type TabID = 'flame-chart' | 'ranked-chart' | 'timeline'; export type Context = {| // Which tab is selected in the Profiler UI? diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js index 94cd201d45759..0e294ffe29cb1 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js @@ -123,7 +123,7 @@ export default function ProfilingImportExportButtons() { className={styles.Input} type="file" onChange={ - selectedTabID === 'scheduling-profiler' + selectedTabID === 'timeline' ? importSchedulingProfilerDataWrapper : importProfilerData } @@ -140,7 +140,7 @@ export default function ProfilingImportExportButtons() { disabled={ isProfiling || !profilerStore.didRecordCommits || - selectedTabID === 'scheduling-profiler' + selectedTabID === 'timeline' } onClick={downloadData} title="Save profile..."> diff --git a/packages/react-devtools-shared/src/dynamicImportCache.js b/packages/react-devtools-shared/src/dynamicImportCache.js index 72c0aa708db50..b31d9602c6b81 100644 --- a/packages/react-devtools-shared/src/dynamicImportCache.js +++ b/packages/react-devtools-shared/src/dynamicImportCache.js @@ -74,7 +74,7 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module { callbacks.add(callback); }, - // Optional property used by Scheduling Profiler: + // Optional property used by Timeline: displayName: `Loading module "${moduleLoaderFunction.name}"`, }; diff --git a/packages/react-devtools-shared/src/hookNamesCache.js b/packages/react-devtools-shared/src/hookNamesCache.js index eee340844fe01..c992f64cf97b2 100644 --- a/packages/react-devtools-shared/src/hookNamesCache.js +++ b/packages/react-devtools-shared/src/hookNamesCache.js @@ -93,7 +93,7 @@ export function loadHookNames( callbacks.add(callback); }, - // Optional property used by Scheduling Profiler: + // Optional property used by Timeline: displayName: `Loading hook names for ${element.displayName || 'Unknown'}`, }; diff --git a/packages/react-devtools-shared/src/inspectedElementCache.js b/packages/react-devtools-shared/src/inspectedElementCache.js index 6442a84b0be64..b7f08ffa523a1 100644 --- a/packages/react-devtools-shared/src/inspectedElementCache.js +++ b/packages/react-devtools-shared/src/inspectedElementCache.js @@ -95,7 +95,7 @@ export function inspectElement( callbacks.add(callback); }, - // Optional property used by Scheduling Profiler: + // Optional property used by Timeline: displayName: `Inspecting ${element.displayName || 'Unknown'}`, }; diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index 7e1461c7a7226..beaf66d0192b4 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -78,7 +78,7 @@ export const IdleLane: Lanes = /* */ 0b0100000000000000000 export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000; -// This function is used for the experimental scheduling profiler (react-devtools-scheduling-profiler) +// This function is used for the experimental timeline (react-devtools-scheduling-profiler) // It should be kept in sync with the Lanes values above. export function getLabelForLane(lane: Lane): string | void { if (enableSchedulingProfiler) { diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index 6b4be15e649f1..4923f607eb8ee 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -78,7 +78,7 @@ export const IdleLane: Lanes = /* */ 0b0100000000000000000 export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000; -// This function is used for the experimental scheduling profiler (react-devtools-scheduling-profiler) +// This function is used for the experimental timeline (react-devtools-scheduling-profiler) // It should be kept in sync with the Lanes values above. export function getLabelForLane(lane: Lane): string | void { if (enableSchedulingProfiler) { diff --git a/packages/react-reconciler/src/SchedulingProfiler.js b/packages/react-reconciler/src/SchedulingProfiler.js index acfb2c3f976d0..58d61067acbfd 100644 --- a/packages/react-reconciler/src/SchedulingProfiler.js +++ b/packages/react-reconciler/src/SchedulingProfiler.js @@ -147,7 +147,7 @@ export function markComponentRenderStarted(fiber: Fiber): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; - // TODO (scheduling profiler) Add component stack id + // TODO (timeline) Add component stack id markAndClear(`--component-render-start-${componentName}`); } } @@ -165,7 +165,7 @@ export function markComponentPassiveEffectMountStarted(fiber: Fiber): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; - // TODO (scheduling profiler) Add component stack id + // TODO (timeline) Add component stack id markAndClear(`--component-passive-effect-mount-start-${componentName}`); } } @@ -183,7 +183,7 @@ export function markComponentPassiveEffectUnmountStarted(fiber: Fiber): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; - // TODO (scheduling profiler) Add component stack id + // TODO (timeline) Add component stack id markAndClear(`--component-passive-effect-unmount-start-${componentName}`); } } @@ -201,7 +201,7 @@ export function markComponentLayoutEffectMountStarted(fiber: Fiber): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; - // TODO (scheduling profiler) Add component stack id + // TODO (timeline) Add component stack id markAndClear(`--component-layout-effect-mount-start-${componentName}`); } } @@ -219,7 +219,7 @@ export function markComponentLayoutEffectUnmountStarted(fiber: Fiber): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; - // TODO (scheduling profiler) Add component stack id + // TODO (timeline) Add component stack id markAndClear(`--component-layout-effect-unmount-start-${componentName}`); } } @@ -254,7 +254,7 @@ export function markComponentErrored( message = thrownValue; } - // TODO (scheduling profiler) Add component stack id + // TODO (timeline) Add component stack id markAndClear(`--error-${componentName}-${phase}-${message}`); } } @@ -287,10 +287,10 @@ export function markComponentSuspended( // Following the non-standard fn.displayName convention, // frameworks like Relay may also annotate Promises with a displayName, // describing what operation/data the thrown Promise is related to. - // When this is available we should pass it along to the Scheduling Profiler. + // When this is available we should pass it along to the Timeline. const displayName = (wakeable: any).displayName || ''; - // TODO (scheduling profiler) Add component stack id + // TODO (timeline) Add component stack id markAndClear( `--suspense-${eventType}-${id}-${componentName}-${phase}-${lanes}-${displayName}`, ); @@ -370,7 +370,7 @@ export function markForceUpdateScheduled(fiber: Fiber, lane: Lane): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; - // TODO (scheduling profiler) Add component stack id + // TODO (timeline) Add component stack id markAndClear(`--schedule-forced-update-${lane}-${componentName}`); } } @@ -380,7 +380,7 @@ export function markStateUpdateScheduled(fiber: Fiber, lane: Lane): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; - // TODO (scheduling profiler) Add component stack id + // TODO (timeline) Add component stack id markAndClear(`--schedule-state-update-${lane}-${componentName}`); } } diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 112c2d10f2cbd..ede13f2c2e8d4 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -16,7 +16,7 @@ export const enableFilterEmptyStringAttributesDOM = false; export const enableDebugTracing = false; // Adds user timing marks for e.g. state updates, suspense, and work loop stuff, -// for an experimental scheduling profiler tool. +// for an experimental timeline tool. export const enableSchedulingProfiler = __PROFILE__; // Helps identify side effects in render-phase lifecycle hooks and setState diff --git a/scripts/rollup/wrappers.js b/scripts/rollup/wrappers.js index c83ecc0fbdb96..3a3556e8d90e0 100644 --- a/scripts/rollup/wrappers.js +++ b/scripts/rollup/wrappers.js @@ -360,7 +360,7 @@ function wrapBundle( case RN_FB_DEV: case RN_FB_PROFILING: // Certain DEV and Profiling bundles should self-register their own module boundaries with DevTools. - // This allows the Scheduling Profiler to de-emphasize (dim) internal stack frames. + // This allows the Timeline to de-emphasize (dim) internal stack frames. source = ` ${registerInternalModuleStart(globalName)} ${source} From 1bf6deb865052111474f2988bb831de13d09c560 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 4 Nov 2021 10:02:06 -0400 Subject: [PATCH 082/109] Renamed packages/react-devtools-scheduling-profiler to packages/react-devtools-timeline (#22691) --- .eslintignore | 4 ++-- .eslintrc.js | 2 +- .gitignore | 2 +- .prettierignore | 4 ++-- packages/react-devtools-shared/src/devtools/views/DevTools.js | 2 +- .../src/devtools/views/Profiler/ClearProfilingDataButton.js | 2 +- .../src/devtools/views/Profiler/Profiler.js | 2 +- .../devtools/views/Profiler/ProfilingImportExportButtons.js | 2 +- .../README.md | 0 .../package.json | 2 +- .../src/CanvasPage.css | 0 .../src/CanvasPage.js | 0 .../src/EventTooltip.css | 0 .../src/EventTooltip.js | 0 .../src/ImportButton.css | 0 .../src/ImportButton.js | 0 .../src/SchedulingProfiler.css | 0 .../src/SchedulingProfiler.js | 0 .../src/SchedulingProfilerContext.js | 0 .../src/constants.js | 0 .../src/content-views/ComponentMeasuresView.js | 0 .../src/content-views/FlamechartView.js | 0 .../src/content-views/NativeEventsView.js | 0 .../src/content-views/NetworkMeasuresView.js | 0 .../src/content-views/ReactMeasuresView.js | 0 .../src/content-views/SchedulingEventsView.js | 0 .../src/content-views/SnapshotsView.js | 0 .../src/content-views/SuspenseEventsView.js | 0 .../src/content-views/ThrownErrorsView.js | 0 .../src/content-views/TimeAxisMarkersView.js | 0 .../src/content-views/UserTimingMarksView.js | 0 .../src/content-views/constants.js | 0 .../src/content-views/index.js | 0 .../content-views/utils/__tests__/__modules__/module-one.js | 0 .../content-views/utils/__tests__/__modules__/module-two.js | 0 .../src/content-views/utils/__tests__/colors-test.js | 0 .../src/content-views/utils/__tests__/moduleFilters-test.js | 0 .../src/content-views/utils/colors.js | 0 .../src/content-views/utils/moduleFilters.js | 0 .../src/content-views/utils/positioning.js | 0 .../src/content-views/utils/text.js | 0 .../src/createDataResourceFromImportedFile.js | 0 .../src/import-worker/InvalidProfileError.js | 0 .../import-worker/__tests__/preprocessData-test.internal.js | 0 .../src/import-worker/importFile.js | 0 .../src/import-worker/importFile.worker.js | 0 .../src/import-worker/index.js | 0 .../src/import-worker/preprocessData.js | 0 .../src/import-worker/readInputData.js | 0 .../src/types.js | 0 .../src/utils/formatting.js | 0 .../src/utils/getBatchRange.js | 0 .../src/utils/useSmartTooltip.js | 0 .../src/view-base/BackgroundColorView.js | 0 .../src/view-base/HorizontalPanAndZoomView.js | 0 .../src/view-base/Surface.js | 0 .../src/view-base/VerticalScrollView.js | 0 .../src/view-base/View.js | 0 .../src/view-base/__tests__/geometry-test.js | 0 .../src/view-base/constants.js | 0 .../src/view-base/geometry.js | 0 .../src/view-base/index.js | 0 .../src/view-base/layouter.js | 0 .../src/view-base/resizable/ResizableView.js | 0 .../src/view-base/resizable/ResizeBarView.js | 0 .../src/view-base/resizable/index.js | 0 .../src/view-base/useCanvasInteraction.js | 0 .../src/view-base/utils/__tests__/clamp-test.js | 0 .../src/view-base/utils/__tests__/scrollState-test.js | 0 .../src/view-base/utils/clamp.js | 0 .../src/view-base/utils/normalizeWheel.js | 0 .../src/view-base/utils/scrollState.js | 0 .../vertical-scroll-overflow/VerticalScrollBarView.js | 0 .../vertical-scroll-overflow/VerticalScrollOverflowView.js | 0 .../src/view-base/vertical-scroll-overflow/index.js | 0 .../vertical-scroll-overflow/withVerticalScrollbarLayout.js | 0 packages/react-reconciler/src/ReactFiberLane.new.js | 4 ++-- packages/react-reconciler/src/ReactFiberLane.old.js | 4 ++-- packages/react-reconciler/src/SchedulingProfiler.js | 2 +- scripts/devtools/configuration.js | 2 +- 80 files changed, 17 insertions(+), 17 deletions(-) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/README.md (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/package.json (94%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/CanvasPage.css (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/CanvasPage.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/EventTooltip.css (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/EventTooltip.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/ImportButton.css (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/ImportButton.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/SchedulingProfiler.css (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/SchedulingProfiler.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/SchedulingProfilerContext.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/constants.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/ComponentMeasuresView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/FlamechartView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/NativeEventsView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/NetworkMeasuresView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/ReactMeasuresView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/SchedulingEventsView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/SnapshotsView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/SuspenseEventsView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/ThrownErrorsView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/TimeAxisMarkersView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/UserTimingMarksView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/constants.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/index.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/utils/__tests__/__modules__/module-one.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/utils/__tests__/__modules__/module-two.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/utils/__tests__/colors-test.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/utils/__tests__/moduleFilters-test.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/utils/colors.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/utils/moduleFilters.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/utils/positioning.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/content-views/utils/text.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/createDataResourceFromImportedFile.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/import-worker/InvalidProfileError.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/import-worker/__tests__/preprocessData-test.internal.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/import-worker/importFile.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/import-worker/importFile.worker.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/import-worker/index.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/import-worker/preprocessData.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/import-worker/readInputData.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/types.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/utils/formatting.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/utils/getBatchRange.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/utils/useSmartTooltip.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/BackgroundColorView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/HorizontalPanAndZoomView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/Surface.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/VerticalScrollView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/View.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/__tests__/geometry-test.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/constants.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/geometry.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/index.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/layouter.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/resizable/ResizableView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/resizable/ResizeBarView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/resizable/index.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/useCanvasInteraction.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/utils/__tests__/clamp-test.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/utils/__tests__/scrollState-test.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/utils/clamp.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/utils/normalizeWheel.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/utils/scrollState.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/vertical-scroll-overflow/VerticalScrollBarView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/vertical-scroll-overflow/VerticalScrollOverflowView.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/vertical-scroll-overflow/index.js (100%) rename packages/{react-devtools-scheduling-profiler => react-devtools-timeline}/src/view-base/vertical-scroll-overflow/withVerticalScrollbarLayout.js (100%) diff --git a/.eslintignore b/.eslintignore index c55cc40866070..7d79ef6923112 100644 --- a/.eslintignore +++ b/.eslintignore @@ -22,5 +22,5 @@ packages/react-devtools-inline/dist packages/react-devtools-shared/src/hooks/__tests__/__source__/__compiled__/ packages/react-devtools-shared/src/hooks/__tests__/__source__/__untransformed__/ packages/react-devtools-shell/dist -packages/react-devtools-scheduling-profiler/dist -packages/react-devtools-scheduling-profiler/static +packages/react-devtools-timeline/dist +packages/react-devtools-timeline/static diff --git a/.eslintrc.js b/.eslintrc.js index b129af49d3d6a..6a0dc2dceae47 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -151,7 +151,7 @@ module.exports = { 'packages/react-test-renderer/**/*.js', 'packages/react-debug-tools/**/*.js', 'packages/react-devtools-extensions/**/*.js', - 'packages/react-devtools-scheduling-profiler/**/*.js', + 'packages/react-devtools-timeline/**/*.js', 'packages/react-native-renderer/**/*.js', 'packages/eslint-plugin-react-hooks/**/*.js', 'packages/jest-react/**/*.js', diff --git a/.gitignore b/.gitignore index 8ba7bbecc2dce..6ec345e172e5e 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,4 @@ packages/react-devtools-extensions/shared/build packages/react-devtools-extensions/.tempUserDataDir packages/react-devtools-inline/dist packages/react-devtools-shell/dist -packages/react-devtools-scheduling-profiler/dist +packages/react-devtools-timeline/dist diff --git a/.prettierignore b/.prettierignore index 50ccffa6a67f4..6f69f7f891d67 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,5 +7,5 @@ packages/react-devtools-inline/dist packages/react-devtools-shared/src/hooks/__tests__/__source__/__compiled__/ packages/react-devtools-shared/src/hooks/__tests__/__source__/__untransformed__/ packages/react-devtools-shell/dist -packages/react-devtools-scheduling-profiler/dist -packages/react-devtools-scheduling-profiler/static \ No newline at end of file +packages/react-devtools-timeline/dist +packages/react-devtools-timeline/static \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index 781ad9d902f68..b0b8f03032924 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -30,7 +30,7 @@ import ViewElementSourceContext from './Components/ViewElementSourceContext'; import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext'; import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; import {ProfilerContextController} from './Profiler/ProfilerContext'; -import {SchedulingProfilerContextController} from 'react-devtools-scheduling-profiler/src/SchedulingProfilerContext'; +import {SchedulingProfilerContextController} from 'react-devtools-timeline/src/SchedulingProfilerContext'; import {ModalDialogContextController} from './ModalDialog'; import ReactLogo from './ReactLogo'; import UnsupportedBridgeProtocolDialog from './UnsupportedBridgeProtocolDialog'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js index a978277ba8697..3f3a8e4987064 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js @@ -13,7 +13,7 @@ import {ProfilerContext} from './ProfilerContext'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import {StoreContext} from '../context'; -import {SchedulingProfilerContext} from 'react-devtools-scheduling-profiler/src/SchedulingProfilerContext'; +import {SchedulingProfilerContext} from 'react-devtools-timeline/src/SchedulingProfilerContext'; export default function ClearProfilingDataButton() { const store = useContext(StoreContext); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index 49107506e48f6..a9f57f5c47d78 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -16,7 +16,7 @@ import ClearProfilingDataButton from './ClearProfilingDataButton'; import CommitFlamegraph from './CommitFlamegraph'; import CommitRanked from './CommitRanked'; import RootSelector from './RootSelector'; -import {SchedulingProfiler} from 'react-devtools-scheduling-profiler/src/SchedulingProfiler'; +import {SchedulingProfiler} from 'react-devtools-timeline/src/SchedulingProfiler'; import RecordToggle from './RecordToggle'; import ReloadAndProfileButton from './ReloadAndProfileButton'; import ProfilingImportExportButtons from './ProfilingImportExportButtons'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js index 0e294ffe29cb1..912f87745e9ea 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js @@ -19,7 +19,7 @@ import { prepareProfilingDataFrontendFromExport, } from './utils'; import {downloadFile} from '../utils'; -import {SchedulingProfilerContext} from 'react-devtools-scheduling-profiler/src/SchedulingProfilerContext'; +import {SchedulingProfilerContext} from 'react-devtools-timeline/src/SchedulingProfilerContext'; import styles from './ProfilingImportExportButtons.css'; diff --git a/packages/react-devtools-scheduling-profiler/README.md b/packages/react-devtools-timeline/README.md similarity index 100% rename from packages/react-devtools-scheduling-profiler/README.md rename to packages/react-devtools-timeline/README.md diff --git a/packages/react-devtools-scheduling-profiler/package.json b/packages/react-devtools-timeline/package.json similarity index 94% rename from packages/react-devtools-scheduling-profiler/package.json rename to packages/react-devtools-timeline/package.json index a74b66fa1d55f..86bab4e260240 100644 --- a/packages/react-devtools-scheduling-profiler/package.json +++ b/packages/react-devtools-timeline/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "react-devtools-scheduling-profiler", + "name": "react-devtools-timeline", "version": "4.21.0", "license": "MIT", "dependencies": { diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.css b/packages/react-devtools-timeline/src/CanvasPage.css similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/CanvasPage.css rename to packages/react-devtools-timeline/src/CanvasPage.css diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-timeline/src/CanvasPage.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/CanvasPage.js rename to packages/react-devtools-timeline/src/CanvasPage.js diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css b/packages/react-devtools-timeline/src/EventTooltip.css similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/EventTooltip.css rename to packages/react-devtools-timeline/src/EventTooltip.css diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js b/packages/react-devtools-timeline/src/EventTooltip.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/EventTooltip.js rename to packages/react-devtools-timeline/src/EventTooltip.js diff --git a/packages/react-devtools-scheduling-profiler/src/ImportButton.css b/packages/react-devtools-timeline/src/ImportButton.css similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/ImportButton.css rename to packages/react-devtools-timeline/src/ImportButton.css diff --git a/packages/react-devtools-scheduling-profiler/src/ImportButton.js b/packages/react-devtools-timeline/src/ImportButton.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/ImportButton.js rename to packages/react-devtools-timeline/src/ImportButton.js diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css b/packages/react-devtools-timeline/src/SchedulingProfiler.css similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css rename to packages/react-devtools-timeline/src/SchedulingProfiler.css diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js b/packages/react-devtools-timeline/src/SchedulingProfiler.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js rename to packages/react-devtools-timeline/src/SchedulingProfiler.js diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js b/packages/react-devtools-timeline/src/SchedulingProfilerContext.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js rename to packages/react-devtools-timeline/src/SchedulingProfilerContext.js diff --git a/packages/react-devtools-scheduling-profiler/src/constants.js b/packages/react-devtools-timeline/src/constants.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/constants.js rename to packages/react-devtools-timeline/src/constants.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js b/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js rename to packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js b/packages/react-devtools-timeline/src/content-views/FlamechartView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js rename to packages/react-devtools-timeline/src/content-views/FlamechartView.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js b/packages/react-devtools-timeline/src/content-views/NativeEventsView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js rename to packages/react-devtools-timeline/src/content-views/NativeEventsView.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/NetworkMeasuresView.js b/packages/react-devtools-timeline/src/content-views/NetworkMeasuresView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/NetworkMeasuresView.js rename to packages/react-devtools-timeline/src/content-views/NetworkMeasuresView.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js b/packages/react-devtools-timeline/src/content-views/ReactMeasuresView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js rename to packages/react-devtools-timeline/src/content-views/ReactMeasuresView.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js b/packages/react-devtools-timeline/src/content-views/SchedulingEventsView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js rename to packages/react-devtools-timeline/src/content-views/SchedulingEventsView.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SnapshotsView.js b/packages/react-devtools-timeline/src/content-views/SnapshotsView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/SnapshotsView.js rename to packages/react-devtools-timeline/src/content-views/SnapshotsView.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js b/packages/react-devtools-timeline/src/content-views/SuspenseEventsView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js rename to packages/react-devtools-timeline/src/content-views/SuspenseEventsView.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ThrownErrorsView.js b/packages/react-devtools-timeline/src/content-views/ThrownErrorsView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/ThrownErrorsView.js rename to packages/react-devtools-timeline/src/content-views/ThrownErrorsView.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/TimeAxisMarkersView.js b/packages/react-devtools-timeline/src/content-views/TimeAxisMarkersView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/TimeAxisMarkersView.js rename to packages/react-devtools-timeline/src/content-views/TimeAxisMarkersView.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js b/packages/react-devtools-timeline/src/content-views/UserTimingMarksView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js rename to packages/react-devtools-timeline/src/content-views/UserTimingMarksView.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js b/packages/react-devtools-timeline/src/content-views/constants.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/constants.js rename to packages/react-devtools-timeline/src/content-views/constants.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/index.js b/packages/react-devtools-timeline/src/content-views/index.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/index.js rename to packages/react-devtools-timeline/src/content-views/index.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-one.js b/packages/react-devtools-timeline/src/content-views/utils/__tests__/__modules__/module-one.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-one.js rename to packages/react-devtools-timeline/src/content-views/utils/__tests__/__modules__/module-one.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-two.js b/packages/react-devtools-timeline/src/content-views/utils/__tests__/__modules__/module-two.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/__modules__/module-two.js rename to packages/react-devtools-timeline/src/content-views/utils/__tests__/__modules__/module-two.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/colors-test.js b/packages/react-devtools-timeline/src/content-views/utils/__tests__/colors-test.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/colors-test.js rename to packages/react-devtools-timeline/src/content-views/utils/__tests__/colors-test.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/moduleFilters-test.js b/packages/react-devtools-timeline/src/content-views/utils/__tests__/moduleFilters-test.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/moduleFilters-test.js rename to packages/react-devtools-timeline/src/content-views/utils/__tests__/moduleFilters-test.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/colors.js b/packages/react-devtools-timeline/src/content-views/utils/colors.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/utils/colors.js rename to packages/react-devtools-timeline/src/content-views/utils/colors.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/moduleFilters.js b/packages/react-devtools-timeline/src/content-views/utils/moduleFilters.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/utils/moduleFilters.js rename to packages/react-devtools-timeline/src/content-views/utils/moduleFilters.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/positioning.js b/packages/react-devtools-timeline/src/content-views/utils/positioning.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/utils/positioning.js rename to packages/react-devtools-timeline/src/content-views/utils/positioning.js diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js b/packages/react-devtools-timeline/src/content-views/utils/text.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js rename to packages/react-devtools-timeline/src/content-views/utils/text.js diff --git a/packages/react-devtools-scheduling-profiler/src/createDataResourceFromImportedFile.js b/packages/react-devtools-timeline/src/createDataResourceFromImportedFile.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/createDataResourceFromImportedFile.js rename to packages/react-devtools-timeline/src/createDataResourceFromImportedFile.js diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/InvalidProfileError.js b/packages/react-devtools-timeline/src/import-worker/InvalidProfileError.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/import-worker/InvalidProfileError.js rename to packages/react-devtools-timeline/src/import-worker/InvalidProfileError.js diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js b/packages/react-devtools-timeline/src/import-worker/__tests__/preprocessData-test.internal.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js rename to packages/react-devtools-timeline/src/import-worker/__tests__/preprocessData-test.internal.js diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/importFile.js b/packages/react-devtools-timeline/src/import-worker/importFile.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/import-worker/importFile.js rename to packages/react-devtools-timeline/src/import-worker/importFile.js diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/importFile.worker.js b/packages/react-devtools-timeline/src/import-worker/importFile.worker.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/import-worker/importFile.worker.js rename to packages/react-devtools-timeline/src/import-worker/importFile.worker.js diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/index.js b/packages/react-devtools-timeline/src/import-worker/index.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/import-worker/index.js rename to packages/react-devtools-timeline/src/import-worker/index.js diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js b/packages/react-devtools-timeline/src/import-worker/preprocessData.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js rename to packages/react-devtools-timeline/src/import-worker/preprocessData.js diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/readInputData.js b/packages/react-devtools-timeline/src/import-worker/readInputData.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/import-worker/readInputData.js rename to packages/react-devtools-timeline/src/import-worker/readInputData.js diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-timeline/src/types.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/types.js rename to packages/react-devtools-timeline/src/types.js diff --git a/packages/react-devtools-scheduling-profiler/src/utils/formatting.js b/packages/react-devtools-timeline/src/utils/formatting.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/utils/formatting.js rename to packages/react-devtools-timeline/src/utils/formatting.js diff --git a/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js b/packages/react-devtools-timeline/src/utils/getBatchRange.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js rename to packages/react-devtools-timeline/src/utils/getBatchRange.js diff --git a/packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js b/packages/react-devtools-timeline/src/utils/useSmartTooltip.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js rename to packages/react-devtools-timeline/src/utils/useSmartTooltip.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/BackgroundColorView.js b/packages/react-devtools-timeline/src/view-base/BackgroundColorView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/BackgroundColorView.js rename to packages/react-devtools-timeline/src/view-base/BackgroundColorView.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js b/packages/react-devtools-timeline/src/view-base/HorizontalPanAndZoomView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js rename to packages/react-devtools-timeline/src/view-base/HorizontalPanAndZoomView.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js b/packages/react-devtools-timeline/src/view-base/Surface.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/Surface.js rename to packages/react-devtools-timeline/src/view-base/Surface.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js b/packages/react-devtools-timeline/src/view-base/VerticalScrollView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js rename to packages/react-devtools-timeline/src/view-base/VerticalScrollView.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/View.js b/packages/react-devtools-timeline/src/view-base/View.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/View.js rename to packages/react-devtools-timeline/src/view-base/View.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/__tests__/geometry-test.js b/packages/react-devtools-timeline/src/view-base/__tests__/geometry-test.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/__tests__/geometry-test.js rename to packages/react-devtools-timeline/src/view-base/__tests__/geometry-test.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/constants.js b/packages/react-devtools-timeline/src/view-base/constants.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/constants.js rename to packages/react-devtools-timeline/src/view-base/constants.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/geometry.js b/packages/react-devtools-timeline/src/view-base/geometry.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/geometry.js rename to packages/react-devtools-timeline/src/view-base/geometry.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/index.js b/packages/react-devtools-timeline/src/view-base/index.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/index.js rename to packages/react-devtools-timeline/src/view-base/index.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js b/packages/react-devtools-timeline/src/view-base/layouter.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/layouter.js rename to packages/react-devtools-timeline/src/view-base/layouter.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/resizable/ResizableView.js b/packages/react-devtools-timeline/src/view-base/resizable/ResizableView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/resizable/ResizableView.js rename to packages/react-devtools-timeline/src/view-base/resizable/ResizableView.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/resizable/ResizeBarView.js b/packages/react-devtools-timeline/src/view-base/resizable/ResizeBarView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/resizable/ResizeBarView.js rename to packages/react-devtools-timeline/src/view-base/resizable/ResizeBarView.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/resizable/index.js b/packages/react-devtools-timeline/src/view-base/resizable/index.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/resizable/index.js rename to packages/react-devtools-timeline/src/view-base/resizable/index.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js b/packages/react-devtools-timeline/src/view-base/useCanvasInteraction.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js rename to packages/react-devtools-timeline/src/view-base/useCanvasInteraction.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/utils/__tests__/clamp-test.js b/packages/react-devtools-timeline/src/view-base/utils/__tests__/clamp-test.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/utils/__tests__/clamp-test.js rename to packages/react-devtools-timeline/src/view-base/utils/__tests__/clamp-test.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/utils/__tests__/scrollState-test.js b/packages/react-devtools-timeline/src/view-base/utils/__tests__/scrollState-test.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/utils/__tests__/scrollState-test.js rename to packages/react-devtools-timeline/src/view-base/utils/__tests__/scrollState-test.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/utils/clamp.js b/packages/react-devtools-timeline/src/view-base/utils/clamp.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/utils/clamp.js rename to packages/react-devtools-timeline/src/view-base/utils/clamp.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/utils/normalizeWheel.js b/packages/react-devtools-timeline/src/view-base/utils/normalizeWheel.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/utils/normalizeWheel.js rename to packages/react-devtools-timeline/src/view-base/utils/normalizeWheel.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/utils/scrollState.js b/packages/react-devtools-timeline/src/view-base/utils/scrollState.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/utils/scrollState.js rename to packages/react-devtools-timeline/src/view-base/utils/scrollState.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/VerticalScrollBarView.js b/packages/react-devtools-timeline/src/view-base/vertical-scroll-overflow/VerticalScrollBarView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/VerticalScrollBarView.js rename to packages/react-devtools-timeline/src/view-base/vertical-scroll-overflow/VerticalScrollBarView.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/VerticalScrollOverflowView.js b/packages/react-devtools-timeline/src/view-base/vertical-scroll-overflow/VerticalScrollOverflowView.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/VerticalScrollOverflowView.js rename to packages/react-devtools-timeline/src/view-base/vertical-scroll-overflow/VerticalScrollOverflowView.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/index.js b/packages/react-devtools-timeline/src/view-base/vertical-scroll-overflow/index.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/index.js rename to packages/react-devtools-timeline/src/view-base/vertical-scroll-overflow/index.js diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/withVerticalScrollbarLayout.js b/packages/react-devtools-timeline/src/view-base/vertical-scroll-overflow/withVerticalScrollbarLayout.js similarity index 100% rename from packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/withVerticalScrollbarLayout.js rename to packages/react-devtools-timeline/src/view-base/vertical-scroll-overflow/withVerticalScrollbarLayout.js diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index beaf66d0192b4..b9073b0770ba8 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -25,7 +25,7 @@ import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; import {clz32} from './clz32'; -// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler. +// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-timeline. // If those values are changed that package should be rebuilt and redeployed. export const TotalLanes = 31; @@ -78,7 +78,7 @@ export const IdleLane: Lanes = /* */ 0b0100000000000000000 export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000; -// This function is used for the experimental timeline (react-devtools-scheduling-profiler) +// This function is used for the experimental timeline (react-devtools-timeline) // It should be kept in sync with the Lanes values above. export function getLabelForLane(lane: Lane): string | void { if (enableSchedulingProfiler) { diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index 4923f607eb8ee..1ef12d4021280 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -25,7 +25,7 @@ import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; import {clz32} from './clz32'; -// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler. +// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-timeline. // If those values are changed that package should be rebuilt and redeployed. export const TotalLanes = 31; @@ -78,7 +78,7 @@ export const IdleLane: Lanes = /* */ 0b0100000000000000000 export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000; -// This function is used for the experimental timeline (react-devtools-scheduling-profiler) +// This function is used for the experimental timeline (react-devtools-timeline) // It should be kept in sync with the Lanes values above. export function getLabelForLane(lane: Lane): string | void { if (enableSchedulingProfiler) { diff --git a/packages/react-reconciler/src/SchedulingProfiler.js b/packages/react-reconciler/src/SchedulingProfiler.js index 58d61067acbfd..0304f39a12e56 100644 --- a/packages/react-reconciler/src/SchedulingProfiler.js +++ b/packages/react-reconciler/src/SchedulingProfiler.js @@ -17,7 +17,7 @@ import { } from 'shared/ReactFeatureFlags'; import ReactVersion from 'shared/ReactVersion'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; -import {SCHEDULING_PROFILER_VERSION} from 'react-devtools-scheduling-profiler/src/constants'; +import {SCHEDULING_PROFILER_VERSION} from 'react-devtools-timeline/src/constants'; import { getLabelForLane as getLabelForLane_old, diff --git a/scripts/devtools/configuration.js b/scripts/devtools/configuration.js index e90b764b36194..98d65baf946a7 100644 --- a/scripts/devtools/configuration.js +++ b/scripts/devtools/configuration.js @@ -6,7 +6,7 @@ const PACKAGE_PATHS = [ 'packages/react-devtools/package.json', 'packages/react-devtools-core/package.json', 'packages/react-devtools-inline/package.json', - 'packages/react-devtools-scheduling-profiler/package.json', + 'packages/react-devtools-timeline/package.json', ]; const MANIFEST_PATHS = [ From 8ca3f567bc3ed56c2101e5c51f968a5339f63093 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 4 Nov 2021 10:40:35 -0400 Subject: [PATCH 083/109] Fix module-boundary wrappers (#22688) --- scripts/rollup/wrappers.js | 32 +++++++++++-------- .../wrappers/registerInternalModuleBegin.js | 10 +----- .../wrappers/registerInternalModuleEnd.js | 10 +----- 3 files changed, 20 insertions(+), 32 deletions(-) rename packages/shared/registerInternalModuleStart.js => scripts/rollup/wrappers/registerInternalModuleBegin.js (51%) rename packages/shared/registerInternalModuleStop.js => scripts/rollup/wrappers/registerInternalModuleEnd.js (50%) diff --git a/scripts/rollup/wrappers.js b/scripts/rollup/wrappers.js index 3a3556e8d90e0..3178c1cf482a1 100644 --- a/scripts/rollup/wrappers.js +++ b/scripts/rollup/wrappers.js @@ -27,24 +27,23 @@ const { const {RECONCILER} = moduleTypes; +const USE_STRICT_HEADER_REGEX = /'use strict';\n+/; + function registerInternalModuleStart(globalName) { - const path = resolve( - __dirname, - '..', - '..', - 'packages/shared/registerInternalModuleStart.js' - ); - return String(readFileSync(path)).trim(); + const path = resolve(__dirname, 'wrappers', 'registerInternalModuleBegin.js'); + const file = readFileSync(path); + return String(file).trim(); } function registerInternalModuleStop(globalName) { - const path = resolve( - __dirname, - '..', - '..', - 'packages/shared/registerInternalModuleStop.js' - ); - return String(readFileSync(path)).trim(); + const path = resolve(__dirname, 'wrappers', 'registerInternalModuleEnd.js'); + const file = readFileSync(path); + + // Remove the 'use strict' directive from the footer. + // This directive is only meaningful when it is the first statement in a file or function. + return String(file) + .replace(USE_STRICT_HEADER_REGEX, '') + .trim(); } const license = ` * Copyright (c) Facebook, Inc. and its affiliates. @@ -359,6 +358,11 @@ function wrapBundle( case RN_OSS_PROFILING: case RN_FB_DEV: case RN_FB_PROFILING: + // Remove the 'use strict' directive from source. + // The module start wrapper will add its own. + // This directive is only meaningful when it is the first statement in a file or function. + source = source.replace(USE_STRICT_HEADER_REGEX, ''); + // Certain DEV and Profiling bundles should self-register their own module boundaries with DevTools. // This allows the Timeline to de-emphasize (dim) internal stack frames. source = ` diff --git a/packages/shared/registerInternalModuleStart.js b/scripts/rollup/wrappers/registerInternalModuleBegin.js similarity index 51% rename from packages/shared/registerInternalModuleStart.js rename to scripts/rollup/wrappers/registerInternalModuleBegin.js index 1bab503c2a6da..a9170ca3e45da 100644 --- a/packages/shared/registerInternalModuleStart.js +++ b/scripts/rollup/wrappers/registerInternalModuleBegin.js @@ -1,14 +1,6 @@ -/** - * 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. - */ +'use strict'; /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ - -// Don't require this file directly; it's embedded by Rollup during build. - if ( typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart === diff --git a/packages/shared/registerInternalModuleStop.js b/scripts/rollup/wrappers/registerInternalModuleEnd.js similarity index 50% rename from packages/shared/registerInternalModuleStop.js rename to scripts/rollup/wrappers/registerInternalModuleEnd.js index 44a69bed9ac37..119fb115a1ceb 100644 --- a/packages/shared/registerInternalModuleStop.js +++ b/scripts/rollup/wrappers/registerInternalModuleEnd.js @@ -1,14 +1,6 @@ -/** - * 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. - */ +'use strict'; /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ - -// Don't require this file directly; it's embedded by Rollup during build. - if ( typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop === From 13455d26d1904519c53686d6f295d4eb50b6c2fc Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 4 Nov 2021 11:40:45 -0400 Subject: [PATCH 084/109] Cleaned up remaining "scheduling profiler" references in DevTools (#22696) --- .../react-devtools-extensions/src/main.js | 2 +- .../react-devtools-inline/src/frontend.js | 2 +- .../src/devtools/store.js | 16 +++---- .../src/devtools/views/DevTools.js | 6 +-- .../Profiler/ClearProfilingDataButton.js | 10 ++-- .../src/devtools/views/Profiler/Profiler.js | 12 ++--- .../Profiler/ProfilingImportExportButtons.js | 10 ++-- .../{SchedulingProfiler.css => Timeline.css} | 0 .../{SchedulingProfiler.js => Timeline.js} | 22 ++++----- ...gProfilerContext.js => TimelineContext.js} | 48 ++++++++----------- 10 files changed, 56 insertions(+), 72 deletions(-) rename packages/react-devtools-timeline/src/{SchedulingProfiler.css => Timeline.css} (100%) rename packages/react-devtools-timeline/src/{SchedulingProfiler.js => Timeline.js} (89%) rename packages/react-devtools-timeline/src/{SchedulingProfilerContext.js => TimelineContext.js} (60%) diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index 36625cf74e30f..015472fa2e89c 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -153,7 +153,7 @@ function createPanelIfReactLoaded() { supportsReloadAndProfile: isChrome || isEdge, supportsProfiling, // At this time, the timeline can only parse Chrome performance profiles. - supportsSchedulingProfiler: isChrome, + supportsTimeline: isChrome, supportsTraceUpdates: true, }); store.profilerStore.profilingData = profilingData; diff --git a/packages/react-devtools-inline/src/frontend.js b/packages/react-devtools-inline/src/frontend.js index 0249fb878f9bb..065bb1238715e 100644 --- a/packages/react-devtools-inline/src/frontend.js +++ b/packages/react-devtools-inline/src/frontend.js @@ -29,7 +29,7 @@ export function createStore(bridge: FrontendBridge, config?: Config): Store { return new Store(bridge, { checkBridgeProtocolCompatibility: true, supportsTraceUpdates: true, - supportsSchedulingProfiler: true, + supportsTimeline: true, supportsNativeInspection: config?.supportsNativeInspection !== false, }); } diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 217abe7f72777..c8b252072dcb4 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -63,9 +63,9 @@ type Config = {| checkBridgeProtocolCompatibility?: boolean, isProfiling?: boolean, supportsNativeInspection?: boolean, - supportsReloadAndProfile?: boolean, - supportsSchedulingProfiler?: boolean, supportsProfiling?: boolean, + supportsReloadAndProfile?: boolean, + supportsTimeline?: boolean, supportsTraceUpdates?: boolean, |}; @@ -162,7 +162,7 @@ export default class Store extends EventEmitter<{| _supportsNativeInspection: boolean = true; _supportsProfiling: boolean = false; _supportsReloadAndProfile: boolean = false; - _supportsSchedulingProfiler: boolean = false; + _supportsTimeline: boolean = false; _supportsTraceUpdates: boolean = false; _unsupportedBridgeProtocol: BridgeProtocol | null = null; @@ -197,7 +197,7 @@ export default class Store extends EventEmitter<{| supportsNativeInspection, supportsProfiling, supportsReloadAndProfile, - supportsSchedulingProfiler, + supportsTimeline, supportsTraceUpdates, } = config; this._supportsNativeInspection = supportsNativeInspection !== false; @@ -207,8 +207,8 @@ export default class Store extends EventEmitter<{| if (supportsReloadAndProfile) { this._supportsReloadAndProfile = true; } - if (supportsSchedulingProfiler) { - this._supportsSchedulingProfiler = true; + if (supportsTimeline) { + this._supportsTimeline = true; } if (supportsTraceUpdates) { this._supportsTraceUpdates = true; @@ -422,8 +422,8 @@ export default class Store extends EventEmitter<{| ); } - get supportsSchedulingProfiler(): boolean { - return this._supportsSchedulingProfiler; + get supportsTimeline(): boolean { + return this._supportsTimeline; } get supportsTraceUpdates(): boolean { diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index b0b8f03032924..b16429b5bcff2 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -30,7 +30,7 @@ import ViewElementSourceContext from './Components/ViewElementSourceContext'; import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext'; import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; import {ProfilerContextController} from './Profiler/ProfilerContext'; -import {SchedulingProfilerContextController} from 'react-devtools-timeline/src/SchedulingProfilerContext'; +import {TimelineContextController} from 'react-devtools-timeline/src/TimelineContext'; import {ModalDialogContextController} from './ModalDialog'; import ReactLogo from './ReactLogo'; import UnsupportedBridgeProtocolDialog from './UnsupportedBridgeProtocolDialog'; @@ -273,7 +273,7 @@ export default function DevTools({ value={fetchFileWithCaching || null}> - +
    -
    +
    diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js index 3f3a8e4987064..f57b4ba8e1731 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js @@ -13,28 +13,26 @@ import {ProfilerContext} from './ProfilerContext'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import {StoreContext} from '../context'; -import {SchedulingProfilerContext} from 'react-devtools-timeline/src/SchedulingProfilerContext'; +import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext'; export default function ClearProfilingDataButton() { const store = useContext(StoreContext); const {didRecordCommits, isProfiling, selectedTabID} = useContext( ProfilerContext, ); - const {clearSchedulingProfilerData, schedulingProfilerData} = useContext( - SchedulingProfilerContext, - ); + const {clearTimelineData, timelineData} = useContext(TimelineContext); const {profilerStore} = store; let doesHaveData = false; if (selectedTabID === 'timeline') { - doesHaveData = schedulingProfilerData !== null; + doesHaveData = timelineData !== null; } else { doesHaveData = didRecordCommits; } const clear = () => { if (selectedTabID === 'timeline') { - clearSchedulingProfilerData(); + clearTimelineData(); } else { profilerStore.clear(); } diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index a9f57f5c47d78..0bd6b0375e076 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -16,7 +16,7 @@ import ClearProfilingDataButton from './ClearProfilingDataButton'; import CommitFlamegraph from './CommitFlamegraph'; import CommitRanked from './CommitRanked'; import RootSelector from './RootSelector'; -import {SchedulingProfiler} from 'react-devtools-timeline/src/SchedulingProfiler'; +import {Timeline} from 'react-devtools-timeline/src/Timeline'; import RecordToggle from './RecordToggle'; import ReloadAndProfileButton from './ReloadAndProfileButton'; import ProfilingImportExportButtons from './ProfilingImportExportButtons'; @@ -43,7 +43,7 @@ function Profiler(_: {||}) { supportsProfiling, } = useContext(ProfilerContext); - const {supportsSchedulingProfiler} = useContext(StoreContext); + const {supportsTimeline} = useContext(StoreContext); let isLegacyProfilerSelected = false; @@ -59,7 +59,7 @@ function Profiler(_: {||}) { view = ; break; case 'timeline': - view = ; + view = ; break; default: break; @@ -116,9 +116,7 @@ function Profiler(_: {||}) { currentTab={selectedTabID} id="Profiler" selectTab={selectTab} - tabs={ - supportsSchedulingProfiler ? tabsWithSchedulingProfiler : tabs - } + tabs={supportsTimeline ? tabsWithTimeline : tabs} type="profiler" /> @@ -160,7 +158,7 @@ const tabs = [ }, ]; -const tabsWithSchedulingProfiler = [ +const tabsWithTimeline = [ ...tabs, null, // Divider/separator { diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js index 912f87745e9ea..574da0639e973 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js @@ -19,7 +19,7 @@ import { prepareProfilingDataFrontendFromExport, } from './utils'; import {downloadFile} from '../utils'; -import {SchedulingProfilerContext} from 'react-devtools-timeline/src/SchedulingProfilerContext'; +import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext'; import styles from './ProfilingImportExportButtons.css'; @@ -29,7 +29,7 @@ export default function ProfilingImportExportButtons() { const {isProfiling, profilingData, rootID, selectedTabID} = useContext( ProfilerContext, ); - const {importSchedulingProfilerData} = useContext(SchedulingProfilerContext); + const {importTimelineData} = useContext(TimelineContext); const store = useContext(StoreContext); const {profilerStore} = store; @@ -108,10 +108,10 @@ export default function ProfilingImportExportButtons() { } }, [modalDialogDispatch, profilerStore]); - const importSchedulingProfilerDataWrapper = event => { + const importTimelineDataWrapper = event => { const input = inputRef.current; if (input !== null && input.files.length > 0) { - importSchedulingProfilerData(input.files[0]); + importTimelineData(input.files[0]); } }; @@ -124,7 +124,7 @@ export default function ProfilingImportExportButtons() { type="file" onChange={ selectedTabID === 'timeline' - ? importSchedulingProfilerDataWrapper + ? importTimelineDataWrapper : importProfilerData } tabIndex={-1} diff --git a/packages/react-devtools-timeline/src/SchedulingProfiler.css b/packages/react-devtools-timeline/src/Timeline.css similarity index 100% rename from packages/react-devtools-timeline/src/SchedulingProfiler.css rename to packages/react-devtools-timeline/src/Timeline.css diff --git a/packages/react-devtools-timeline/src/SchedulingProfiler.js b/packages/react-devtools-timeline/src/Timeline.js similarity index 89% rename from packages/react-devtools-timeline/src/SchedulingProfiler.js rename to packages/react-devtools-timeline/src/Timeline.js index 98cd61d312447..4b6d21f8a8f7a 100644 --- a/packages/react-devtools-timeline/src/SchedulingProfiler.js +++ b/packages/react-devtools-timeline/src/Timeline.js @@ -21,18 +21,16 @@ import { } from 'react'; import {SettingsContext} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext'; import {updateColorsToMatchTheme} from './content-views/constants'; -import {SchedulingProfilerContext} from './SchedulingProfilerContext'; +import {TimelineContext} from './TimelineContext'; import ImportButton from './ImportButton'; import CanvasPage from './CanvasPage'; -import styles from './SchedulingProfiler.css'; +import styles from './Timeline.css'; -export function SchedulingProfiler(_: {||}) { - const { - importSchedulingProfilerData, - schedulingProfilerData, - viewState, - } = useContext(SchedulingProfilerContext); +export function Timeline(_: {||}) { + const {importTimelineData, timelineData, viewState} = useContext( + TimelineContext, + ); const ref = useRef(null); @@ -63,17 +61,17 @@ export function SchedulingProfiler(_: {||}) { return (
    - {schedulingProfilerData ? ( + {timelineData ? ( }> ) : ( - + )}
    ); diff --git a/packages/react-devtools-timeline/src/SchedulingProfilerContext.js b/packages/react-devtools-timeline/src/TimelineContext.js similarity index 60% rename from packages/react-devtools-timeline/src/SchedulingProfilerContext.js rename to packages/react-devtools-timeline/src/TimelineContext.js index ff81482c09e74..ebfac421e2c3e 100644 --- a/packages/react-devtools-timeline/src/SchedulingProfilerContext.js +++ b/packages/react-devtools-timeline/src/TimelineContext.js @@ -15,33 +15,28 @@ import type {HorizontalScrollStateChangeCallback, ViewState} from './types'; import type {DataResource} from './createDataResourceFromImportedFile'; export type Context = {| - clearSchedulingProfilerData: () => void, - importSchedulingProfilerData: (file: File) => void, - schedulingProfilerData: DataResource | null, + clearTimelineData: () => void, + importTimelineData: (file: File) => void, + timelineData: DataResource | null, viewState: ViewState, |}; -const SchedulingProfilerContext = createContext( - ((null: any): Context), -); -SchedulingProfilerContext.displayName = 'SchedulingProfilerContext'; +const TimelineContext = createContext(((null: any): Context)); +TimelineContext.displayName = 'TimelineContext'; type Props = {| children: React$Node, |}; -function SchedulingProfilerContextController({children}: Props) { - const [ - schedulingProfilerData, - setSchedulingProfilerData, - ] = useState(null); +function TimelineContextController({children}: Props) { + const [timelineData, setTimelineData] = useState(null); - const clearSchedulingProfilerData = useCallback(() => { - setSchedulingProfilerData(null); + const clearTimelineData = useCallback(() => { + setTimelineData(null); }, []); - const importSchedulingProfilerData = useCallback((file: File) => { - setSchedulingProfilerData(createDataResourceFromImportedFile(file)); + const importTimelineData = useCallback((file: File) => { + setTimelineData(createDataResourceFromImportedFile(file)); }, []); // Recreate view state any time new profiling data is imported. @@ -75,28 +70,23 @@ function SchedulingProfilerContextController({children}: Props) { }, viewToMutableViewStateMap: new Map(), }; - }, [schedulingProfilerData]); + }, [timelineData]); const value = useMemo( () => ({ - clearSchedulingProfilerData, - importSchedulingProfilerData, - schedulingProfilerData, + clearTimelineData, + importTimelineData, + timelineData, viewState, }), - [ - clearSchedulingProfilerData, - importSchedulingProfilerData, - schedulingProfilerData, - viewState, - ], + [clearTimelineData, importTimelineData, timelineData, viewState], ); return ( - + {children} - + ); } -export {SchedulingProfilerContext, SchedulingProfilerContextController}; +export {TimelineContext, TimelineContextController}; From 54f6ae9b1c0489784f6a95bbe26ffec31816d74a Mon Sep 17 00:00:00 2001 From: Konstantin Popov Date: Fri, 5 Nov 2021 02:49:06 +0300 Subject: [PATCH 085/109] Fix small typos (#22701) --- packages/react-devtools-shared/src/__tests__/console-test.js | 2 +- packages/react-devtools-shared/src/backend/views/utils.js | 2 +- .../update-stable-version-numbers.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/console-test.js b/packages/react-devtools-shared/src/__tests__/console-test.js index fef2d9674b6fd..1129c153267cb 100644 --- a/packages/react-devtools-shared/src/__tests__/console-test.js +++ b/packages/react-devtools-shared/src/__tests__/console-test.js @@ -69,7 +69,7 @@ describe('console', () => { ); } - it('should not patch console methods that are not explicitly overriden', () => { + it('should not patch console methods that are not explicitly overridden', () => { expect(fakeConsole.error).not.toBe(mockError); expect(fakeConsole.info).toBe(mockInfo); expect(fakeConsole.log).toBe(mockLog); diff --git a/packages/react-devtools-shared/src/backend/views/utils.js b/packages/react-devtools-shared/src/backend/views/utils.js index e332c81a0af86..a677c7447efc7 100644 --- a/packages/react-devtools-shared/src/backend/views/utils.js +++ b/packages/react-devtools-shared/src/backend/views/utils.js @@ -50,7 +50,7 @@ export function getBoundingClientRectWithBorderOffset(node: HTMLElement) { right: dimensions.borderRight, // This width and height won't get used by mergeRectOffsets (since this // is not the first rect in the array), but we set them so that this - // object typechecks as a ClientRect. + // object type checks as a ClientRect. width: 0, height: 0, }, diff --git a/scripts/release/prepare-release-from-npm-commands/update-stable-version-numbers.js b/scripts/release/prepare-release-from-npm-commands/update-stable-version-numbers.js index 16bec66b6076d..47a0907bafad5 100644 --- a/scripts/release/prepare-release-from-npm-commands/update-stable-version-numbers.js +++ b/scripts/release/prepare-release-from-npm-commands/update-stable-version-numbers.js @@ -132,7 +132,7 @@ const run = async ({cwd, packages, version}, versionsMap) => { let diff = ''; let numFilesModified = 0; - // Find-and-replace hard coded version (in built JS) for renderers. + // Find-and-replace hardcoded version (in built JS) for renderers. for (let i = 0; i < packages.length; i++) { const packageName = packages[i]; const packagePath = join(nodeModulesPath, packageName); From ee069065db449194a62a5c0dc2f8f6b61fa5e1b8 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Fri, 5 Nov 2021 15:58:35 +0000 Subject: [PATCH 086/109] devtools: Display root type for root updates in "what caused this update?" (#22599) --- .../__snapshots__/profilingCache-test.js.snap | 82 +++++++++---------- .../src/backend/renderer.js | 4 + .../src/devtools/views/Profiler/Updaters.js | 6 +- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap index 8d7eeb4255a94..92555b0761cd2 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap @@ -44,7 +44,7 @@ Object { "timestamp": 16, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -91,7 +91,7 @@ Object { "timestamp": 15, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -127,7 +127,7 @@ Object { "timestamp": 18, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -190,7 +190,7 @@ Object { "timestamp": 12, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -258,7 +258,7 @@ Object { "timestamp": 25, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -307,7 +307,7 @@ Object { "timestamp": 35, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -346,7 +346,7 @@ Object { "timestamp": 45, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -455,7 +455,7 @@ Object { "timestamp": 12, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -562,7 +562,7 @@ Object { "timestamp": 25, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -632,7 +632,7 @@ Object { "timestamp": 35, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -683,7 +683,7 @@ Object { "timestamp": 45, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -954,7 +954,7 @@ Object { "timestamp": 11, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -1042,7 +1042,7 @@ Object { "timestamp": 22, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -1149,7 +1149,7 @@ Object { "timestamp": 35, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -1359,7 +1359,7 @@ Object { "timestamp": 13, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -1405,7 +1405,7 @@ Object { "timestamp": 34, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -1441,7 +1441,7 @@ Object { "timestamp": 44, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -1624,7 +1624,7 @@ Object { "timestamp": 24, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 13, "key": null, @@ -1714,7 +1714,7 @@ Object { "timestamp": 34, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 7, "key": null, @@ -1892,7 +1892,7 @@ Object { "timestamp": 13, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -1962,7 +1962,7 @@ Object { "timestamp": 34, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -2013,7 +2013,7 @@ Object { "timestamp": 44, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -2256,7 +2256,7 @@ Object { "timestamp": 24, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 13, "key": null, @@ -2343,7 +2343,7 @@ Object { "timestamp": 34, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 7, "key": null, @@ -2464,7 +2464,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -2541,7 +2541,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -2584,7 +2584,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -2699,7 +2699,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -2740,7 +2740,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -2811,7 +2811,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -2878,7 +2878,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -3029,7 +3029,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -3094,7 +3094,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -3271,7 +3271,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -3441,7 +3441,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -3523,7 +3523,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -3604,7 +3604,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -3739,7 +3739,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -4011,7 +4011,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -4147,7 +4147,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, @@ -4282,7 +4282,7 @@ Object { "timestamp": 0, "updaters": Array [ Object { - "displayName": "Anonymous", + "displayName": "render()", "hocDisplayNames": null, "id": 1, "key": null, diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 218b255388371..9d2005522da4b 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -425,6 +425,10 @@ export function getInternalReactConstants( getDisplayName(resolvedType, 'Anonymous') ); case HostRoot: + const fiberRoot = fiber.stateNode; + if (fiberRoot != null && fiberRoot._debugRootType !== null) { + return fiberRoot._debugRootType; + } return null; case HostComponent: return type; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Updaters.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Updaters.js index 9e2518612e455..d35ff86d5895c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Updaters.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Updaters.js @@ -14,6 +14,7 @@ import * as React from 'react'; import {useContext} from 'react'; import {ProfilerContext} from './ProfilerContext'; import styles from './Updaters.css'; +import {ElementTypeRoot} from '../../../types'; export type Props = {| commitTree: CommitTree, @@ -26,8 +27,9 @@ export default function Updaters({commitTree, updaters}: Props) { const children = updaters.length > 0 ? ( updaters.map((serializedElement: SerializedElement) => { - const {displayName, id, key} = serializedElement; - const isVisibleInTree = commitTree.nodes.has(id); + const {displayName, id, key, type} = serializedElement; + const isVisibleInTree = + commitTree.nodes.has(id) && type !== ElementTypeRoot; if (isVisibleInTree) { return (