From 546ab4c726c42ee316f6917c8a6fc211897452d7 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 19:45:03 -0400 Subject: [PATCH 01/26] synchronously hydrate discrete events in capture phase and dont replay them --- ...DOMServerPartialHydration-test.internal.js | 80 ++++++++-- ...MServerSelectiveHydration-test.internal.js | 93 ++++++------ .../src/events/ReactDOMEventListener.js | 80 +++++----- .../src/events/ReactDOMEventReplaying.js | 139 +----------------- .../DOMPluginEventSystem-test.internal.js | 7 +- 5 files changed, 159 insertions(+), 240 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 9f954f4581d3c..5a34c2ca89ebf 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1905,8 +1905,15 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(clicks).toBe(1); + // Clicks aren't replayed + expect(clicks).toBe(0); + expect(container.textContent).toBe('Click meHello'); + await act(async () => { + a.click(); + }); + // Now click went through + expect(clicks).toBe(1); expect(container.textContent).toBe('Hello'); document.body.removeChild(container); @@ -1991,8 +1998,13 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - expect(onEvent).toHaveBeenCalledTimes(2); + // Clicks are not replayed + expect(onEvent).toHaveBeenCalledTimes(0); + await act(async () => { + a.click(); + }); + expect(onEvent).toHaveBeenCalledTimes(1); document.body.removeChild(container); }); @@ -2072,7 +2084,13 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(clicks).toBe(2); + // clicks are not replayed + expect(clicks).toBe(0); + + await act(async () => { + a.click(); + }); + expect(clicks).toBe(1); document.body.removeChild(container); }); @@ -2158,7 +2176,16 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(onEvent).toHaveBeenCalledTimes(2); + + // Stays 0 because we don't replay clicks + expect(onEvent).toHaveBeenCalledTimes(0); + + await act(async () => { + a.click(); + }); + + // Clicks now go through after resolving + expect(onEvent).toHaveBeenCalledTimes(1); document.body.removeChild(container); }); @@ -2231,6 +2258,13 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); + // Stays 0 because we don't replay click events + expect(clicksOnChild).toBe(0); + + await act(async () => { + span.click(); + }); + // Now click events go through expect(clicksOnChild).toBe(1); // This will be zero due to the stopPropagation. expect(clicksOnParent).toBe(0); @@ -2309,8 +2343,12 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - // We're now full hydrated. + // We're now full hydrated but we don't replay clicks + expect(clicks).toBe(0); + await act(async () => { + a.click(); + }); expect(clicks).toBe(1); document.body.removeChild(parentContainer); @@ -2351,7 +2389,7 @@ describe('ReactDOMServerPartialHydration', () => { onMouseLeave={() => ops.push('Mouse Leave First')} /> {/* We suspend after to test what happens when we eager - attach the listener. */} + attach the listener. */} @@ -2396,9 +2434,11 @@ describe('ReactDOMServerPartialHydration', () => { expect(ops).toEqual([]); // Resolving the second promise so that rendering can complete. - suspend2 = false; - resolve2(); - await promise2; + await act(async () => { + suspend2 = false; + resolve2(); + await promise2; + }); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -2407,10 +2447,12 @@ describe('ReactDOMServerPartialHydration', () => { // able to replay it now. expect(ops).toEqual(['Mouse Enter Second']); - // Resolving the first promise has no effect now. - suspend1 = false; - resolve1(); - await promise1; + await act(async () => { + // Resolving the first promise has no effect now. + suspend1 = false; + resolve1(); + await promise1; + }); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -2580,6 +2622,18 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); + // Submit event is not replayed + expect(submits).toBe(0); + expect(container.textContent).toBe('Click meHello'); + + await act(async () => { + form.dispatchEvent( + new Event('submit', { + bubbles: true, + }), + ); + }); + // Submit event has now gone through expect(submits).toBe(1); expect(container.textContent).toBe('Hello'); document.body.removeChild(container); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 76d9b43d07f61..111614e9cd422 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -129,8 +129,10 @@ describe('ReactDOMServerSelectiveHydration', () => { Scheduler.unstable_yieldValue(text); return ( { + Scheduler.unstable_yieldValue('Capture Clicked ' + text); + }} onClick={e => { - e.preventDefault(); Scheduler.unstable_yieldValue('Clicked ' + text); }}> {text} @@ -172,13 +174,15 @@ describe('ReactDOMServerSelectiveHydration', () => { // This should synchronously hydrate the root App and the second suspense // boundary. - const result = dispatchClickEvent(span); - - // The event should have been canceled because we called preventDefault. - expect(result).toBe(false); + dispatchClickEvent(span); // We rendered App, B and then invoked the event without rendering A. - expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']); + expect(Scheduler).toHaveYielded([ + 'App', + 'B', + 'Capture Clicked B', + 'Clicked B', + ]); // After continuing the scheduler, we finally hydrate A. expect(Scheduler).toFlushAndYield(['A']); @@ -256,7 +260,7 @@ describe('ReactDOMServerSelectiveHydration', () => { }); expect(Scheduler).toHaveYielded([ 'App', - // Continuing rendering will render B next. + // B and C don't suspense so they are rendered immediately 'B', 'C', ]); @@ -266,9 +270,9 @@ describe('ReactDOMServerSelectiveHydration', () => { resolve(); await promise; }); - // After the click, we should prioritize D and the Click first, + // After the click, we should prioritize hydrating D // and only after that render A and C. - expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); + expect(Scheduler).toHaveYielded(['D', 'A']); document.body.removeChild(container); }); @@ -338,12 +342,13 @@ describe('ReactDOMServerSelectiveHydration', () => { // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); - // This click target cannot be hydrated yet because the first is Suspended. + // A and D cannot be hydrated yet because they Suspended. dispatchClickEvent(spanA); - dispatchClickEvent(spanC); dispatchClickEvent(spanD); - expect(Scheduler).toHaveYielded(['App']); + // C can be immediately hydrated in capture phase in time for it to be clicked + dispatchClickEvent(spanC); + expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']); await act(async () => { suspend = false; @@ -351,15 +356,11 @@ describe('ReactDOMServerSelectiveHydration', () => { await promise; }); - // We should prioritize hydrating A, C and D first since we clicked in + // We should prioritize hydrating A then D first since we clicked in // them. Only after they're done will we hydrate B. expect(Scheduler).toHaveYielded([ 'A', - 'Clicked A', - 'C', - 'Clicked C', 'D', - 'Clicked D', // B should render last since it wasn't clicked. 'B', ]); @@ -512,14 +513,15 @@ describe('ReactDOMServerSelectiveHydration', () => { }); expect(Scheduler).toHaveYielded(['App', 'B', 'C']); - // After the click, we should prioritize D and the Click first, - // and only after that render A and C. + // After the click, we should prioritize D, + // and only after that render A await act(async () => { suspend = false; resolve(); await promise; }); - expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); + // No click is yielded since we don't replay clicks + expect(Scheduler).toHaveYielded(['D', 'A']); document.body.removeChild(container); }); @@ -596,10 +598,12 @@ describe('ReactDOMServerSelectiveHydration', () => { // This click target cannot be hydrated yet because the first is Suspended. createEventTarget(spanA).virtualclick(); - createEventTarget(spanC).virtualclick(); createEventTarget(spanD).virtualclick(); - expect(Scheduler).toHaveYielded(['App']); + // C can be immediately hydrated in capture phase in time for click + createEventTarget(spanC).virtualclick(); + + expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']); await act(async () => { suspend = false; @@ -611,11 +615,7 @@ describe('ReactDOMServerSelectiveHydration', () => { // them. Only after they're done will we hydrate B. expect(Scheduler).toHaveYielded([ 'A', - 'Clicked A', - 'C', - 'Clicked C', 'D', - 'Clicked D', // B should render last since it wasn't clicked. 'B', ]); @@ -699,7 +699,8 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchMouseHoverEvent(spanB, spanD); dispatchMouseHoverEvent(spanC, spanB); - expect(Scheduler).toHaveYielded(['App']); + // We can hydrate B and C now. + expect(Scheduler).toHaveYielded(['App', 'B', 'C']); await act(async () => { suspend = false; @@ -713,14 +714,7 @@ describe('ReactDOMServerSelectiveHydration', () => { // the same time since B was already scheduled. // This is ok because it will at least not continue for nested // boundary. See the next test below. - expect(Scheduler).toHaveYielded([ - 'D', - 'Clicked D', - 'B', // Ideally this should be later. - 'C', - 'Hover C', - 'A', - ]); + expect(Scheduler).toHaveYielded(['D', 'A']); document.body.removeChild(container); }); @@ -796,16 +790,22 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchMouseHoverEvent(spanB, spanD); dispatchMouseHoverEvent(spanC, spanB); - suspend = false; - resolve(); - await promise; + await act(async () => { + suspend = false; + resolve(); + await promise; + }); + + // C renders before B since its the current hover target + // B renders because its priority was increased when it was hovered over + expect(Scheduler).toHaveYielded(['App', 'C', 'B', 'Hover C', 'D', 'A']); // We should prioritize hydrating D first because we clicked it. // Next we should hydrate C since that's the current hover target. // Next it doesn't matter if we hydrate A or B first but as an // implementation detail we're currently hydrating B first since // we at one point hovered over it and we never deprioritized it. - expect(Scheduler).toFlushAndYield(['App', 'C', 'Hover C', 'A', 'B', 'D']); + expect(Scheduler).toFlushAndYield([]); document.body.removeChild(container); }); @@ -942,20 +942,17 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchClickEvent(spanC); expect(Scheduler).toHaveYielded([ - // Hydrate C first since we clicked it. + // Hydrate A and B since we hovered + // then Hydrate C since we clicked it. + 'A', + 'a', + 'B', + 'b', 'C', 'c', ]); expect(Scheduler).toFlushAndYield([ - // Finish hydration of A since we forced it to hydrate. - 'A', - 'a', - // Also, hydrate B since we hovered over it. - // It's not important which one comes first. A or B. - // As long as they both happen before the Idle update. - 'B', - 'b', // Begin the Idle update again. 'App', 'AA', diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index dfe3c2e3c00c0..1f9794c9817f5 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -13,12 +13,11 @@ import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig'; import type {DOMEventName} from '../events/DOMEventNames'; import { - isReplayableDiscreteEvent, - queueDiscreteEvent, - hasQueuedDiscreteEvents, + attemptSynchronousEventHydration, clearIfContinuousEvent, queueIfContinuousEvent, } from './ReactDOMEventReplaying'; +import { enableSelectiveHydration } from 'shared/ReactFeatureFlags'; import { getNearestMountedFiber, getContainerFromFiber, @@ -28,7 +27,10 @@ import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags'; import {type EventSystemFlags, IS_CAPTURE_PHASE} from './EventSystemFlags'; import getEventTarget from './getEventTarget'; -import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; +import { + getInstanceFromNode, + getClosestInstanceFromNode, +} from '../client/ReactDOMComponentTree'; import {dispatchEventForPluginEventSystem} from './DOMPluginEventSystem'; @@ -158,25 +160,7 @@ export function dispatchEvent( // to filter them out until we fix the logic to handle them correctly. const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0; - if ( - allowReplay && - hasQueuedDiscreteEvents() && - isReplayableDiscreteEvent(domEventName) - ) { - // If we already have a queue of discrete events, and this is another discrete - // event, then we can't dispatch it regardless of its target, since they - // need to dispatch in order. - queueDiscreteEvent( - null, // Flags that we're not actually blocked on anything as far as we know. - domEventName, - eventSystemFlags, - targetContainer, - nativeEvent, - ); - return; - } - - const blockedOn = attemptToDispatchEvent( + let blockedOn = attemptToDispatchEvent( domEventName, eventSystemFlags, targetContainer, @@ -184,40 +168,48 @@ export function dispatchEvent( ); if (blockedOn === null) { - // We successfully dispatched this event. if (allowReplay) { + // We successfully dispatched this event. clearIfContinuousEvent(domEventName, nativeEvent); } return; } - if (allowReplay) { - if (isReplayableDiscreteEvent(domEventName)) { - // This this to be replayed later once the target is available. - queueDiscreteEvent( - blockedOn, + if ( + allowReplay && + queueIfContinuousEvent( + blockedOn, + domEventName, + eventSystemFlags, + targetContainer, + nativeEvent, + ) + ) { + return; + } + + // Synchronously hydrate non-replayable / non-continuous events + if (enableSelectiveHydration) { + while (blockedOn !== null) { + const fiber = getInstanceFromNode(blockedOn); + if (fiber !== null) { + attemptSynchronousEventHydration(fiber); + } + const nextBlockedOn = attemptToDispatchEvent( domEventName, eventSystemFlags, targetContainer, nativeEvent, ); - return; - } - if ( - queueIfContinuousEvent( - blockedOn, - domEventName, - eventSystemFlags, - targetContainer, - nativeEvent, - ) - ) { - return; + if (nextBlockedOn === blockedOn) { + break; + } + blockedOn = nextBlockedOn; } - // We need to clear only if we didn't queue because - // queueing is accumulative. - clearIfContinuousEvent(domEventName, nativeEvent); } + // We need to clear only if we didn't queue because + // queueing is accumulative. + clearIfContinuousEvent(domEventName, nativeEvent); // This is not replayable so we'll invoke it but without a target, // in case the event system needs to trace it. diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index eab9b4650c108..f3a7eca1ba88c 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -44,6 +44,10 @@ export function setAttemptDiscreteHydration(fn: (fiber: Object) => void) { attemptDiscreteHydration = fn; } +export function attemptSynchronousEventHydration(fiber: Object): void { + attemptSynchronousHydration(fiber); +} + let attemptContinuousHydration: (fiber: Object) => void; export function setAttemptContinuousHydration(fn: (fiber: Object) => void) { @@ -92,9 +96,6 @@ type QueuedReplayableEvent = {| let hasScheduledReplayAttempt = false; -// The queue of discrete events to be replayed. -const queuedDiscreteEvents: Array = []; - // Indicates if any continuous event targets are non-null for early bailout. const hasAnyQueuedContinuousEvents: boolean = false; // The last of each continuous event type. We only need to replay the last one @@ -114,49 +115,10 @@ type QueuedHydrationTarget = {| |}; const queuedExplicitHydrationTargets: Array = []; -export function hasQueuedDiscreteEvents(): boolean { - return queuedDiscreteEvents.length > 0; -} - export function hasQueuedContinuousEvents(): boolean { return hasAnyQueuedContinuousEvents; } -const discreteReplayableEvents: Array = [ - 'mousedown', - 'mouseup', - 'touchcancel', - 'touchend', - 'touchstart', - 'auxclick', - 'dblclick', - 'pointercancel', - 'pointerdown', - 'pointerup', - 'dragend', - 'dragstart', - 'drop', - 'compositionend', - 'compositionstart', - 'keydown', - 'keypress', - 'keyup', - 'input', - 'textInput', // Intentionally camelCase - 'copy', - 'cut', - 'paste', - 'click', - 'change', - 'contextmenu', - 'reset', - 'submit', -]; - -export function isReplayableDiscreteEvent(eventType: DOMEventName): boolean { - return discreteReplayableEvents.indexOf(eventType) > -1; -} - function createQueuedReplayableEvent( blockedOn: null | Container | SuspenseInstance, domEventName: DOMEventName, @@ -173,47 +135,6 @@ function createQueuedReplayableEvent( }; } -export function queueDiscreteEvent( - blockedOn: null | Container | SuspenseInstance, - domEventName: DOMEventName, - eventSystemFlags: EventSystemFlags, - targetContainer: EventTarget, - nativeEvent: AnyNativeEvent, -): void { - const queuedEvent = createQueuedReplayableEvent( - blockedOn, - domEventName, - eventSystemFlags, - targetContainer, - nativeEvent, - ); - queuedDiscreteEvents.push(queuedEvent); - if (enableSelectiveHydration) { - if (queuedDiscreteEvents.length === 1) { - // If this was the first discrete event, we might be able to - // synchronously unblock it so that preventDefault still works. - while (queuedEvent.blockedOn !== null) { - const fiber = getInstanceFromNode(queuedEvent.blockedOn); - if (fiber === null) { - break; - } - attemptSynchronousHydration(fiber); - if (queuedEvent.blockedOn === null) { - // We got unblocked by hydration. Let's try again. - replayUnblockedEvents(); - // If we're reblocked, on an inner boundary, we might need - // to attempt hydrating that one. - continue; - } else { - // We're still blocked from hydration, we have to give up - // and replay later. - break; - } - } - } - } -} - // Resets the replaying for this type of continuous event to no event. export function clearIfContinuousEvent( domEventName: DOMEventName, @@ -483,42 +404,7 @@ function attemptReplayContinuousQueuedEventInMap( function replayUnblockedEvents() { hasScheduledReplayAttempt = false; - // First replay discrete events. - while (queuedDiscreteEvents.length > 0) { - const nextDiscreteEvent = queuedDiscreteEvents[0]; - if (nextDiscreteEvent.blockedOn !== null) { - // We're still blocked. - // Increase the priority of this boundary to unblock - // the next discrete event. - const fiber = getInstanceFromNode(nextDiscreteEvent.blockedOn); - if (fiber !== null) { - attemptDiscreteHydration(fiber); - } - break; - } - const targetContainers = nextDiscreteEvent.targetContainers; - while (targetContainers.length > 0) { - const targetContainer = targetContainers[0]; - const nextBlockedOn = attemptToDispatchEvent( - nextDiscreteEvent.domEventName, - nextDiscreteEvent.eventSystemFlags, - targetContainer, - nextDiscreteEvent.nativeEvent, - ); - if (nextBlockedOn !== null) { - // We're still blocked. Try again later. - nextDiscreteEvent.blockedOn = nextBlockedOn; - break; - } - // This target container was successfully dispatched. Try the next. - targetContainers.shift(); - } - if (nextDiscreteEvent.blockedOn === null) { - // We've successfully replayed the first event. Let's try the next one. - queuedDiscreteEvents.shift(); - } - } - // Next replay any continuous events. + // Replay continuous events. if (queuedFocus !== null && attemptReplayContinuousQueuedEvent(queuedFocus)) { queuedFocus = null; } @@ -551,21 +437,6 @@ function scheduleCallbackIfUnblocked( export function retryIfBlockedOn( unblocked: Container | SuspenseInstance, ): void { - // Mark anything that was blocked on this as no longer blocked - // and eligible for a replay. - if (queuedDiscreteEvents.length > 0) { - scheduleCallbackIfUnblocked(queuedDiscreteEvents[0], unblocked); - // This is a exponential search for each boundary that commits. I think it's - // worth it because we expect very few discrete events to queue up and once - // we are actually fully unblocked it will be fast to replay them. - for (let i = 1; i < queuedDiscreteEvents.length; i++) { - const queuedEvent = queuedDiscreteEvents[i]; - if (queuedEvent.blockedOn === unblocked) { - queuedEvent.blockedOn = null; - } - } - } - if (queuedFocus !== null) { scheduleCallbackIfUnblocked(queuedFocus, unblocked); } diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js index 4f2df6fffc495..1798a464ac288 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -647,8 +647,13 @@ describe('DOMPluginEventSystem', () => { await promise; }); - // We're now full hydrated. + // We're now fully hydrated. But the click isn't replayed. + expect(clicks).toBe(0); + await act(async () => { + a.click(); + }); + // Clicks can go through now expect(clicks).toBe(1); document.body.removeChild(parentContainer); From 7aeaeb5d7cc36906974848f6f3fce8e74ac015b6 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 19:54:49 -0400 Subject: [PATCH 02/26] test?? --- packages/react-dom/src/client/ReactDOM.js | 3 -- .../src/events/ReactDOMEventListener.js | 2 +- .../src/events/ReactDOMEventReplaying.js | 6 ---- .../src/ReactFiberReconciler.js | 5 --- .../src/ReactFiberReconciler.new.js | 14 -------- .../src/ReactFiberReconciler.old.js | 36 ++++++------------- 6 files changed, 12 insertions(+), 54 deletions(-) diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index a1bb39c929d43..4484754a2fea6 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -28,7 +28,6 @@ import { flushControlled, injectIntoDevTools, attemptSynchronousHydration, - attemptDiscreteHydration, attemptContinuousHydration, attemptHydrationAtCurrentPriority, } from 'react-reconciler/src/ReactFiberReconciler'; @@ -54,7 +53,6 @@ import { import {restoreControlledState} from './ReactDOMComponent'; import { setAttemptSynchronousHydration, - setAttemptDiscreteHydration, setAttemptContinuousHydration, setAttemptHydrationAtCurrentPriority, queueExplicitHydrationTarget, @@ -69,7 +67,6 @@ import { } from '../events/ReactDOMControlledComponent'; setAttemptSynchronousHydration(attemptSynchronousHydration); -setAttemptDiscreteHydration(attemptDiscreteHydration); setAttemptContinuousHydration(attemptContinuousHydration); setAttemptHydrationAtCurrentPriority(attemptHydrationAtCurrentPriority); setGetCurrentUpdatePriority(getCurrentUpdatePriority); diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 1f9794c9817f5..8850be4f7bed6 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -17,7 +17,7 @@ import { clearIfContinuousEvent, queueIfContinuousEvent, } from './ReactDOMEventReplaying'; -import { enableSelectiveHydration } from 'shared/ReactFeatureFlags'; +import {enableSelectiveHydration} from 'shared/ReactFeatureFlags'; import { getNearestMountedFiber, getContainerFromFiber, diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index f3a7eca1ba88c..6a29f3fa777d1 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -38,12 +38,6 @@ export function setAttemptSynchronousHydration(fn: (fiber: Object) => void) { attemptSynchronousHydration = fn; } -let attemptDiscreteHydration: (fiber: Object) => void; - -export function setAttemptDiscreteHydration(fn: (fiber: Object) => void) { - attemptDiscreteHydration = fn; -} - export function attemptSynchronousEventHydration(fiber: Object): void { attemptSynchronousHydration(fiber); } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 6e992ddd7d3b1..7dac786438236 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -26,7 +26,6 @@ import { flushPassiveEffects as flushPassiveEffects_old, getPublicRootInstance as getPublicRootInstance_old, attemptSynchronousHydration as attemptSynchronousHydration_old, - attemptDiscreteHydration as attemptDiscreteHydration_old, attemptContinuousHydration as attemptContinuousHydration_old, attemptHydrationAtCurrentPriority as attemptHydrationAtCurrentPriority_old, findHostInstance as findHostInstance_old, @@ -63,7 +62,6 @@ import { flushPassiveEffects as flushPassiveEffects_new, getPublicRootInstance as getPublicRootInstance_new, attemptSynchronousHydration as attemptSynchronousHydration_new, - attemptDiscreteHydration as attemptDiscreteHydration_new, attemptContinuousHydration as attemptContinuousHydration_new, attemptHydrationAtCurrentPriority as attemptHydrationAtCurrentPriority_new, findHostInstance as findHostInstance_new, @@ -119,9 +117,6 @@ export const getPublicRootInstance = enableNewReconciler export const attemptSynchronousHydration = enableNewReconciler ? attemptSynchronousHydration_new : attemptSynchronousHydration_old; -export const attemptDiscreteHydration = enableNewReconciler - ? attemptDiscreteHydration_new - : attemptDiscreteHydration_old; export const attemptContinuousHydration = enableNewReconciler ? attemptContinuousHydration_new : attemptContinuousHydration_old; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index 95844fcbf2e8d..db6895b002e4b 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -390,20 +390,6 @@ function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) { } } -export function attemptDiscreteHydration(fiber: Fiber): void { - if (fiber.tag !== SuspenseComponent) { - // We ignore HostRoots here because we can't increase - // their priority and they should not suspend on I/O, - // since you have to wrap anything that might suspend in - // Suspense. - return; - } - const eventTime = requestEventTime(); - const lane = SyncLane; - scheduleUpdateOnFiber(fiber, lane, eventTime); - markRetryLaneIfNotHydrated(fiber, lane); -} - export function attemptContinuousHydration(fiber: Fiber): void { if (fiber.tag !== SuspenseComponent) { // We ignore HostRoots here because we can't increase diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 802b1bc97111b..db6895b002e4b 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -18,8 +18,8 @@ import type { } from './ReactFiberHostConfig'; import type {RendererInspectionConfig} from './ReactFiberHostConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; -import type {Lane} from './ReactFiberLane.old'; -import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; +import type {Lane} from './ReactFiberLane.new'; +import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import { findCurrentHostFiber, @@ -43,9 +43,9 @@ import { processChildContext, emptyContextObject, isContextProvider as isLegacyContextProvider, -} from './ReactFiberContext.old'; -import {createFiberRoot} from './ReactFiberRoot.old'; -import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.old'; +} from './ReactFiberContext.new'; +import {createFiberRoot} from './ReactFiberRoot.new'; +import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.new'; import { requestEventTime, requestUpdateLane, @@ -58,12 +58,12 @@ import { deferredUpdates, discreteUpdates, flushPassiveEffects, -} from './ReactFiberWorkLoop.old'; +} from './ReactFiberWorkLoop.new'; import { createUpdate, enqueueUpdate, entangleTransitions, -} from './ReactUpdateQueue.old'; +} from './ReactUpdateQueue.new'; import { isRendering as ReactCurrentFiberIsRendering, current as ReactCurrentFiberCurrent, @@ -77,20 +77,20 @@ import { NoTimestamp, getHighestPriorityPendingLanes, higherPriorityLane, -} from './ReactFiberLane.old'; +} from './ReactFiberLane.new'; import { getCurrentUpdatePriority, runWithPriority, -} from './ReactEventPriorities.old'; +} from './ReactEventPriorities.new'; import { scheduleRefresh, scheduleRoot, setRefreshHandler, findHostInstancesForRefresh, -} from './ReactFiberHotReloading.old'; +} from './ReactFiberHotReloading.new'; import {markRenderScheduled} from './SchedulingProfiler'; import ReactVersion from 'shared/ReactVersion'; -export {registerMutableSourceForHydration} from './ReactMutableSource.old'; +export {registerMutableSourceForHydration} from './ReactMutableSource.new'; export {createPortal} from './ReactPortal'; export { createComponentSelector, @@ -390,20 +390,6 @@ function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) { } } -export function attemptDiscreteHydration(fiber: Fiber): void { - if (fiber.tag !== SuspenseComponent) { - // We ignore HostRoots here because we can't increase - // their priority and they should not suspend on I/O, - // since you have to wrap anything that might suspend in - // Suspense. - return; - } - const eventTime = requestEventTime(); - const lane = SyncLane; - scheduleUpdateOnFiber(fiber, lane, eventTime); - markRetryLaneIfNotHydrated(fiber, lane); -} - export function attemptContinuousHydration(fiber: Fiber): void { if (fiber.tag !== SuspenseComponent) { // We ignore HostRoots here because we can't increase From 89572cbe87a2b46034b288692e2b325f70948f2e Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 19:56:15 -0400 Subject: [PATCH 03/26] replace old with new --- .../src/ReactFiberReconciler.old.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index db6895b002e4b..b562cf814a406 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -18,8 +18,8 @@ import type { } from './ReactFiberHostConfig'; import type {RendererInspectionConfig} from './ReactFiberHostConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; -import type {Lane} from './ReactFiberLane.new'; -import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; +import type { Lane } from './ReactFiberLane.old'; +import type { SuspenseState } from './ReactFiberSuspenseComponent.old'; import { findCurrentHostFiber, @@ -43,9 +43,9 @@ import { processChildContext, emptyContextObject, isContextProvider as isLegacyContextProvider, -} from './ReactFiberContext.new'; -import {createFiberRoot} from './ReactFiberRoot.new'; -import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.new'; +} from './ReactFiberContext.old'; +import { createFiberRoot } from './ReactFiberRoot.old'; +import { injectInternals, onScheduleRoot } from './ReactFiberDevToolsHook.old'; import { requestEventTime, requestUpdateLane, @@ -58,12 +58,12 @@ import { deferredUpdates, discreteUpdates, flushPassiveEffects, -} from './ReactFiberWorkLoop.new'; +} from './ReactFiberWorkLoop.old'; import { createUpdate, enqueueUpdate, entangleTransitions, -} from './ReactUpdateQueue.new'; +} from './ReactUpdateQueue.old'; import { isRendering as ReactCurrentFiberIsRendering, current as ReactCurrentFiberCurrent, @@ -77,20 +77,20 @@ import { NoTimestamp, getHighestPriorityPendingLanes, higherPriorityLane, -} from './ReactFiberLane.new'; +} from './ReactFiberLane.old'; import { getCurrentUpdatePriority, runWithPriority, -} from './ReactEventPriorities.new'; +} from './ReactEventPriorities.old'; import { scheduleRefresh, scheduleRoot, setRefreshHandler, findHostInstancesForRefresh, -} from './ReactFiberHotReloading.new'; +} from './ReactFiberHotReloading.old'; import {markRenderScheduled} from './SchedulingProfiler'; import ReactVersion from 'shared/ReactVersion'; -export {registerMutableSourceForHydration} from './ReactMutableSource.new'; +export { registerMutableSourceForHydration } from './ReactMutableSource.old'; export {createPortal} from './ReactPortal'; export { createComponentSelector, From 3e10694133d83c84bbfe8ed66ed5f59759d54b16 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 19:57:54 -0400 Subject: [PATCH 04/26] prettier --- .../react-reconciler/src/ReactFiberReconciler.old.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index b562cf814a406..81126418f3255 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -18,8 +18,8 @@ import type { } from './ReactFiberHostConfig'; import type {RendererInspectionConfig} from './ReactFiberHostConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; -import type { Lane } from './ReactFiberLane.old'; -import type { SuspenseState } from './ReactFiberSuspenseComponent.old'; +import type {Lane} from './ReactFiberLane.old'; +import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; import { findCurrentHostFiber, @@ -44,8 +44,8 @@ import { emptyContextObject, isContextProvider as isLegacyContextProvider, } from './ReactFiberContext.old'; -import { createFiberRoot } from './ReactFiberRoot.old'; -import { injectInternals, onScheduleRoot } from './ReactFiberDevToolsHook.old'; +import {createFiberRoot} from './ReactFiberRoot.old'; +import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.old'; import { requestEventTime, requestUpdateLane, @@ -90,7 +90,7 @@ import { } from './ReactFiberHotReloading.old'; import {markRenderScheduled} from './SchedulingProfiler'; import ReactVersion from 'shared/ReactVersion'; -export { registerMutableSourceForHydration } from './ReactMutableSource.old'; +export {registerMutableSourceForHydration} from './ReactMutableSource.old'; export {createPortal} from './ReactPortal'; export { createComponentSelector, From 1307b90defb60653ff2fca361fa3613c459c82c3 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Wed, 29 Sep 2021 09:38:48 -0400 Subject: [PATCH 05/26] update test --- ...MServerSelectiveHydration-test.internal.js | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 111614e9cd422..910a5ca3153a2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -778,10 +778,8 @@ describe('ReactDOMServerSelectiveHydration', () => { suspend = true; - // A and D will be suspended. We'll click on D which should take - // priority, after we unsuspend. - const root = ReactDOM.createRoot(container, {hydrate: true}); - root.render(); + // A and D will be suspended. + const root = ReactDOM.hydrateRoot(container, ); // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); @@ -790,22 +788,18 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchMouseHoverEvent(spanB, spanD); dispatchMouseHoverEvent(spanC, spanB); + // C renders before B since its the current hover target + // B renders because its priority was increased when it was hovered over + expect(Scheduler).toHaveYielded(['App', 'C', 'B', 'Hover C']); + await act(async () => { suspend = false; resolve(); await promise; }); - // C renders before B since its the current hover target - // B renders because its priority was increased when it was hovered over - expect(Scheduler).toHaveYielded(['App', 'C', 'B', 'Hover C', 'D', 'A']); - - // We should prioritize hydrating D first because we clicked it. - // Next we should hydrate C since that's the current hover target. - // Next it doesn't matter if we hydrate A or B first but as an - // implementation detail we're currently hydrating B first since - // we at one point hovered over it and we never deprioritized it. - expect(Scheduler).toFlushAndYield([]); + // Finally D and A render + expect(Scheduler).toHaveYielded(['D', 'A']); document.body.removeChild(container); }); From f5ff2fd157ee4ba4b9e46f952df68a8f0599df19 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Wed, 29 Sep 2021 09:54:05 -0400 Subject: [PATCH 06/26] remove unused var --- .../__tests__/ReactDOMServerSelectiveHydration-test.internal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 910a5ca3153a2..2f917c85f468c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -779,7 +779,7 @@ describe('ReactDOMServerSelectiveHydration', () => { suspend = true; // A and D will be suspended. - const root = ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, ); // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); From b98f04bd465bb021f69754190d28d1b1231d2fa5 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Fri, 1 Oct 2021 11:28:08 -0400 Subject: [PATCH 07/26] gate changes --- ...DOMServerPartialHydration-test.internal.js | 159 ++++++++------ ...MServerSelectiveHydration-test.internal.js | 196 +++++++++++++----- packages/react-dom/src/client/ReactDOM.js | 12 +- .../src/events/ReactDOMEventListener.js | 82 ++++++-- .../src/events/ReactDOMEventReplaying.js | 159 +++++++++++++- .../DOMPluginEventSystem-test.internal.js | 7 +- .../src/ReactFiberReconciler.js | 5 + .../src/ReactFiberReconciler.new.js | 33 ++- .../src/ReactFiberReconciler.old.js | 33 ++- packages/shared/ReactFeatureFlags.js | 2 + .../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 | 1 + .../shared/forks/ReactFeatureFlags.testing.js | 1 + .../forks/ReactFeatureFlags.testing.www.js | 1 + .../forks/ReactFeatureFlags.www-dynamic.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + 19 files changed, 522 insertions(+), 175 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 5a34c2ca89ebf..ec54c21f73c1f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1831,6 +1831,7 @@ describe('ReactDOMServerPartialHydration', () => { expect(newSpan.className).toBe('hi'); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a hydrated node until it commits', async () => { let suspend = false; let resolve; @@ -1905,21 +1906,15 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - // Clicks aren't replayed - expect(clicks).toBe(0); - expect(container.textContent).toBe('Click meHello'); - - await act(async () => { - a.click(); - }); - // Now click went through expect(clicks).toBe(1); + expect(container.textContent).toBe('Hello'); document.body.removeChild(container); }); // @gate www + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a hydrated event handle until it commits', async () => { const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; @@ -1998,16 +1993,12 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - // Clicks are not replayed - expect(onEvent).toHaveBeenCalledTimes(0); + expect(onEvent).toHaveBeenCalledTimes(2); - await act(async () => { - a.click(); - }); - expect(onEvent).toHaveBeenCalledTimes(1); document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('invokes discrete events on nested suspense boundaries in a root (legacy system)', async () => { let suspend = false; let resolve; @@ -2084,18 +2075,13 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - // clicks are not replayed - expect(clicks).toBe(0); - - await act(async () => { - a.click(); - }); - expect(clicks).toBe(1); + expect(clicks).toBe(2); document.body.removeChild(container); }); // @gate www + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('invokes discrete events on nested suspense boundaries in a root (createEventHandle)', async () => { let suspend = false; let isServerRendering = true; @@ -2176,20 +2162,12 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - - // Stays 0 because we don't replay clicks - expect(onEvent).toHaveBeenCalledTimes(0); - - await act(async () => { - a.click(); - }); - - // Clicks now go through after resolving - expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledTimes(2); document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke the parent of dehydrated boundary event', async () => { let suspend = false; let resolve; @@ -2258,13 +2236,6 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - // Stays 0 because we don't replay click events - expect(clicksOnChild).toBe(0); - - await act(async () => { - span.click(); - }); - // Now click events go through expect(clicksOnChild).toBe(1); // This will be zero due to the stopPropagation. expect(clicksOnParent).toBe(0); @@ -2272,6 +2243,7 @@ describe('ReactDOMServerPartialHydration', () => { document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => { let suspend = false; let resolve; @@ -2343,17 +2315,14 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - // We're now full hydrated but we don't replay clicks - expect(clicks).toBe(0); + // We're now full hydrated. - await act(async () => { - a.click(); - }); expect(clicks).toBe(1); document.body.removeChild(parentContainer); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('blocks only on the last continuous event (legacy system)', async () => { let suspend1 = false; let resolve1; @@ -2389,7 +2358,7 @@ describe('ReactDOMServerPartialHydration', () => { onMouseLeave={() => ops.push('Mouse Leave First')} /> {/* We suspend after to test what happens when we eager - attach the listener. */} + attach the listener. */} @@ -2434,11 +2403,9 @@ describe('ReactDOMServerPartialHydration', () => { expect(ops).toEqual([]); // Resolving the second promise so that rendering can complete. - await act(async () => { - suspend2 = false; - resolve2(); - await promise2; - }); + suspend2 = false; + resolve2(); + await promise2; Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -2447,12 +2414,10 @@ describe('ReactDOMServerPartialHydration', () => { // able to replay it now. expect(ops).toEqual(['Mouse Enter Second']); - await act(async () => { - // Resolving the first promise has no effect now. - suspend1 = false; - resolve1(); - await promise1; - }); + // Resolving the first promise has no effect now. + suspend1 = false; + resolve1(); + await promise1; Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -2543,6 +2508,7 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).not.toBe(null); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('regression test: does not overfire non-bubbling browser events', async () => { let suspend = false; let resolve; @@ -2622,18 +2588,6 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - // Submit event is not replayed - expect(submits).toBe(0); - expect(container.textContent).toBe('Click meHello'); - - await act(async () => { - form.dispatchEvent( - new Event('submit', { - bubbles: true, - }), - ); - }); - // Submit event has now gone through expect(submits).toBe(1); expect(container.textContent).toBe('Hello'); document.body.removeChild(container); @@ -2725,4 +2679,75 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).toBe(span); expect(ref.current.innerHTML).toBe('Hidden child'); }); + + // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + it('Does not replay discrete events', async () => { + let suspend = false; + let resolve; + const promise = new Promise(resolvePromise => (resolve = resolvePromise)); + + let clicks = 0; + + function Button() { + if (suspend) { + throw promise; + } + return ( + { + clicks++; + }}> + Click me + + ); + } + + function App() { + return ( +
+ +
+ ); + } + + const finalHTML = ReactDOMServer.renderToString(); + const container = document.createElement('div'); + container.innerHTML = finalHTML; + document.body.appendChild(container); + + const a = container.getElementsByTagName('a')[0]; + + // On the client we don't have all data yet but we want to start + // hydrating anyway. + suspend = true; + const root = ReactDOM.hydrateRoot(container, ); + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + + expect(container.textContent).toBe('Click me'); + + // We're now partially hydrated. + await act(async () => { + a.click(); + }); + expect(clicks).toBe(0); + + // Resolving the promise so that rendering can complete. + await act(async () => { + suspend = false; + resolve(); + await promise; + jest.runAllTimers(); + Scheduler.unstable_flushAll(); + }); + + // Event was not replayed + expect(clicks).toBe(0); + + expect(container.textContent).toBe('Click me'); + + document.body.removeChild(container); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 2f917c85f468c..2e5e55a07bd26 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -129,10 +129,8 @@ describe('ReactDOMServerSelectiveHydration', () => { Scheduler.unstable_yieldValue(text); return ( { - Scheduler.unstable_yieldValue('Capture Clicked ' + text); - }} onClick={e => { + e.preventDefault(); Scheduler.unstable_yieldValue('Clicked ' + text); }}> {text} @@ -174,15 +172,13 @@ describe('ReactDOMServerSelectiveHydration', () => { // This should synchronously hydrate the root App and the second suspense // boundary. - dispatchClickEvent(span); + const result = dispatchClickEvent(span); + + // The event should have been canceled because we called preventDefault. + expect(result).toBe(false); // We rendered App, B and then invoked the event without rendering A. - expect(Scheduler).toHaveYielded([ - 'App', - 'B', - 'Capture Clicked B', - 'Clicked B', - ]); + expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']); // After continuing the scheduler, we finally hydrate A. expect(Scheduler).toFlushAndYield(['A']); @@ -190,6 +186,7 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri if sync did not work first time', async () => { let suspend = false; let resolve; @@ -260,7 +257,7 @@ describe('ReactDOMServerSelectiveHydration', () => { }); expect(Scheduler).toHaveYielded([ 'App', - // B and C don't suspense so they are rendered immediately + // Continuing rendering will render B next. 'B', 'C', ]); @@ -270,13 +267,14 @@ describe('ReactDOMServerSelectiveHydration', () => { resolve(); await promise; }); - // After the click, we should prioritize hydrating D + // After the click, we should prioritize D and the Click first, // and only after that render A and C. - expect(Scheduler).toHaveYielded(['D', 'A']); + expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri for secondary discrete events', async () => { let suspend = false; let resolve; @@ -342,13 +340,12 @@ describe('ReactDOMServerSelectiveHydration', () => { // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); - // A and D cannot be hydrated yet because they Suspended. + // This click target cannot be hydrated yet because the first is Suspended. dispatchClickEvent(spanA); + dispatchClickEvent(spanC); dispatchClickEvent(spanD); - // C can be immediately hydrated in capture phase in time for it to be clicked - dispatchClickEvent(spanC); - expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']); + expect(Scheduler).toHaveYielded(['App']); await act(async () => { suspend = false; @@ -356,11 +353,15 @@ describe('ReactDOMServerSelectiveHydration', () => { await promise; }); - // We should prioritize hydrating A then D first since we clicked in + // We should prioritize hydrating A, C and D first since we clicked in // them. Only after they're done will we hydrate B. expect(Scheduler).toHaveYielded([ 'A', + 'Clicked A', + 'C', + 'Clicked C', 'D', + 'Clicked D', // B should render last since it wasn't clicked. 'B', ]); @@ -438,6 +439,7 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate www + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri if sync did not work first time (createEventHandle)', async () => { let suspend = false; let isServerRendering = true; @@ -513,20 +515,20 @@ describe('ReactDOMServerSelectiveHydration', () => { }); expect(Scheduler).toHaveYielded(['App', 'B', 'C']); - // After the click, we should prioritize D, - // and only after that render A + // After the click, we should prioritize D and the Click first, + // and only after that render A and C. await act(async () => { suspend = false; resolve(); await promise; }); - // No click is yielded since we don't replay clicks - expect(Scheduler).toHaveYielded(['D', 'A']); + expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); document.body.removeChild(container); }); // @gate www + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri for secondary discrete events (createEventHandle)', async () => { const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; @@ -598,12 +600,10 @@ describe('ReactDOMServerSelectiveHydration', () => { // This click target cannot be hydrated yet because the first is Suspended. createEventTarget(spanA).virtualclick(); - createEventTarget(spanD).virtualclick(); - - // C can be immediately hydrated in capture phase in time for click createEventTarget(spanC).virtualclick(); + createEventTarget(spanD).virtualclick(); - expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']); + expect(Scheduler).toHaveYielded(['App']); await act(async () => { suspend = false; @@ -615,7 +615,11 @@ describe('ReactDOMServerSelectiveHydration', () => { // them. Only after they're done will we hydrate B. expect(Scheduler).toHaveYielded([ 'A', + 'Clicked A', + 'C', + 'Clicked C', 'D', + 'Clicked D', // B should render last since it wasn't clicked. 'B', ]); @@ -623,6 +627,7 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates the hovered targets as higher priority for continuous events', async () => { let suspend = false; let resolve; @@ -699,8 +704,7 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchMouseHoverEvent(spanB, spanD); dispatchMouseHoverEvent(spanC, spanB); - // We can hydrate B and C now. - expect(Scheduler).toHaveYielded(['App', 'B', 'C']); + expect(Scheduler).toHaveYielded(['App']); await act(async () => { suspend = false; @@ -714,11 +718,19 @@ describe('ReactDOMServerSelectiveHydration', () => { // the same time since B was already scheduled. // This is ok because it will at least not continue for nested // boundary. See the next test below. - expect(Scheduler).toHaveYielded(['D', 'A']); + expect(Scheduler).toHaveYielded([ + 'D', + 'Clicked D', + 'B', // Ideally this should be later. + 'C', + 'Hover C', + 'A', + ]); document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates the last target path first for continuous events', async () => { let suspend = false; let resolve; @@ -778,8 +790,10 @@ describe('ReactDOMServerSelectiveHydration', () => { suspend = true; - // A and D will be suspended. - ReactDOM.hydrateRoot(container, ); + // A and D will be suspended. We'll click on D which should take + // priority, after we unsuspend. + const root = ReactDOM.createRoot(container, {hydrate: true}); + root.render(); // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); @@ -788,18 +802,16 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchMouseHoverEvent(spanB, spanD); dispatchMouseHoverEvent(spanC, spanB); - // C renders before B since its the current hover target - // B renders because its priority was increased when it was hovered over - expect(Scheduler).toHaveYielded(['App', 'C', 'B', 'Hover C']); + suspend = false; + resolve(); + await promise; - await act(async () => { - suspend = false; - resolve(); - await promise; - }); - - // Finally D and A render - expect(Scheduler).toHaveYielded(['D', 'A']); + // We should prioritize hydrating D first because we clicked it. + // Next we should hydrate C since that's the current hover target. + // Next it doesn't matter if we hydrate A or B first but as an + // implementation detail we're currently hydrating B first since + // we at one point hovered over it and we never deprioritized it. + expect(Scheduler).toFlushAndYield(['App', 'C', 'Hover C', 'A', 'B', 'D']); document.body.removeChild(container); }); @@ -854,6 +866,7 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate experimental || www + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates before an update even if hydration moves away from it', async () => { function Child({text}) { Scheduler.unstable_yieldValue(text); @@ -936,17 +949,20 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchClickEvent(spanC); expect(Scheduler).toHaveYielded([ - // Hydrate A and B since we hovered - // then Hydrate C since we clicked it. - 'A', - 'a', - 'B', - 'b', + // Hydrate C first since we clicked it. 'C', 'c', ]); expect(Scheduler).toFlushAndYield([ + // Finish hydration of A since we forced it to hydrate. + 'A', + 'a', + // Also, hydrate B since we hovered over it. + // It's not important which one comes first. A or B. + // As long as they both happen before the Idle update. + 'B', + 'b', // Begin the Idle update again. 'App', 'AA', @@ -961,4 +977,88 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); + + // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + it('Fires capture event handlers and native events if content is hydratable during discrete event', async () => { + function Child({text}) { + Scheduler.unstable_yieldValue(text); + const ref = React.useRef(); + React.useLayoutEffect(() => { + ref.current && + ref.current.addEventListener('click', () => { + Scheduler.unstable_yieldValue('Native Click ' + text); + }); + }, [text]); + return ( + { + Scheduler.unstable_yieldValue('Capture Clicked ' + text); + }} + onClick={e => { + Scheduler.unstable_yieldValue('Clicked ' + text); + }}> + {text} + + ); + } + + function App() { + Scheduler.unstable_yieldValue('App'); + return ( +
+ + + + + + +
+ ); + } + + spyOnDev(console, 'error'); + const finalHTML = ReactDOMServer.renderToString(); + expect(console.error).toHaveBeenCalledTimes(2); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'useLayoutEffect does nothing on the server', + ); + expect(console.error.calls.argsFor(1)[0]).toContain( + 'useLayoutEffect does nothing on the server', + ); + + expect(Scheduler).toHaveYielded(['App', 'A', 'B']); + + const container = document.createElement('div'); + // We need this to be in the document since we'll dispatch events on it. + document.body.appendChild(container); + + container.innerHTML = finalHTML; + + const span = container.getElementsByTagName('span')[1]; + + const root = ReactDOM.hydrateRoot(container, ); + + // Nothing has been hydrated so far. + expect(Scheduler).toHaveYielded([]); + + // This should synchronously hydrate the root App and the second suspense + // boundary. + dispatchClickEvent(span); + + // We rendered App, B and then invoked the event without rendering A. + expect(Scheduler).toHaveYielded([ + 'App', + 'B', + 'Capture Clicked B', + 'Native Click B', + 'Clicked B', + ]); + + // After continuing the scheduler, we finally hydrate A. + expect(Scheduler).toFlushAndYield(['A']); + + document.body.removeChild(container); + expect(console.error).toHaveBeenCalledTimes(2); + }); }); diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 4484754a2fea6..8e37995cb7364 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -28,6 +28,7 @@ import { flushControlled, injectIntoDevTools, attemptSynchronousHydration, + attemptDiscreteHydration, attemptContinuousHydration, attemptHydrationAtCurrentPriority, } from 'react-reconciler/src/ReactFiberReconciler'; @@ -38,7 +39,6 @@ import { import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal'; import {canUseDOM} from 'shared/ExecutionEnvironment'; import ReactVersion from 'shared/ReactVersion'; -import invariant from 'shared/invariant'; import { warnUnstableRenderSubtreeIntoContainer, enableNewReconciler, @@ -53,6 +53,7 @@ import { import {restoreControlledState} from './ReactDOMComponent'; import { setAttemptSynchronousHydration, + setAttemptDiscreteHydration, setAttemptContinuousHydration, setAttemptHydrationAtCurrentPriority, queueExplicitHydrationTarget, @@ -67,6 +68,7 @@ import { } from '../events/ReactDOMControlledComponent'; setAttemptSynchronousHydration(attemptSynchronousHydration); +setAttemptDiscreteHydration(attemptDiscreteHydration); setAttemptContinuousHydration(attemptContinuousHydration); setAttemptHydrationAtCurrentPriority(attemptHydrationAtCurrentPriority); setGetCurrentUpdatePriority(getCurrentUpdatePriority); @@ -105,10 +107,10 @@ function createPortal( container: Container, key: ?string = null, ): React$Portal { - invariant( - isValidContainer(container), - 'Target container is not a DOM element.', - ); + if (!isValidContainer(container)) { + throw new Error('Target container is not a DOM element.'); + } + // TODO: pass ReactDOM portal implementation as third argument // $FlowFixMe The Flow type is opaque but there's no way to actually create it. return createPortalImpl(children, container, null, key); diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 8850be4f7bed6..2d4a4e6885c8a 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -11,13 +11,18 @@ import type {AnyNativeEvent} from '../events/PluginModuleType'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig'; import type {DOMEventName} from '../events/DOMEventNames'; - import { - attemptSynchronousEventHydration, + enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + enableSelectiveHydration, +} from 'shared/ReactFeatureFlags'; +import { + isReplayableDiscreteEvent, + queueDiscreteEvent, + hasQueuedDiscreteEvents, clearIfContinuousEvent, queueIfContinuousEvent, + attemptSynchronousHydration, } from './ReactDOMEventReplaying'; -import {enableSelectiveHydration} from 'shared/ReactFeatureFlags'; import { getNearestMountedFiber, getContainerFromFiber, @@ -160,6 +165,24 @@ export function dispatchEvent( // to filter them out until we fix the logic to handle them correctly. const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0; + if ( + allowReplay && + hasQueuedDiscreteEvents() && + isReplayableDiscreteEvent(domEventName) + ) { + // If we already have a queue of discrete events, and this is another discrete + // event, then we can't dispatch it regardless of its target, since they + // need to dispatch in order. + queueDiscreteEvent( + null, // Flags that we're not actually blocked on anything as far as we know. + domEventName, + eventSystemFlags, + targetContainer, + nativeEvent, + ); + return; + } + let blockedOn = attemptToDispatchEvent( domEventName, eventSystemFlags, @@ -168,32 +191,52 @@ export function dispatchEvent( ); if (blockedOn === null) { + // We successfully dispatched this event. if (allowReplay) { - // We successfully dispatched this event. clearIfContinuousEvent(domEventName, nativeEvent); } return; } - if ( - allowReplay && - queueIfContinuousEvent( - blockedOn, - domEventName, - eventSystemFlags, - targetContainer, - nativeEvent, - ) - ) { - return; + if (allowReplay) { + if ( + !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay && + isReplayableDiscreteEvent(domEventName) + ) { + // This this to be replayed later once the target is available. + queueDiscreteEvent( + blockedOn, + domEventName, + eventSystemFlags, + targetContainer, + nativeEvent, + ); + return; + } + if ( + queueIfContinuousEvent( + blockedOn, + domEventName, + eventSystemFlags, + targetContainer, + nativeEvent, + ) + ) { + return; + } + // We need to clear only if we didn't queue because + // queueing is accumulative. + clearIfContinuousEvent(domEventName, nativeEvent); } - // Synchronously hydrate non-replayable / non-continuous events - if (enableSelectiveHydration) { + if ( + enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay && + enableSelectiveHydration + ) { while (blockedOn !== null) { const fiber = getInstanceFromNode(blockedOn); if (fiber !== null) { - attemptSynchronousEventHydration(fiber); + attemptSynchronousHydration(fiber); } const nextBlockedOn = attemptToDispatchEvent( domEventName, @@ -207,9 +250,6 @@ export function dispatchEvent( blockedOn = nextBlockedOn; } } - // We need to clear only if we didn't queue because - // queueing is accumulative. - clearIfContinuousEvent(domEventName, nativeEvent); // This is not replayable so we'll invoke it but without a target, // in case the event system needs to trace it. diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index 6a29f3fa777d1..76f5958351f2e 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -14,7 +14,10 @@ import type {EventSystemFlags} from './EventSystemFlags'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities'; -import {enableSelectiveHydration} from 'shared/ReactFeatureFlags'; +import { + enableSelectiveHydration, + enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, +} from 'shared/ReactFeatureFlags'; import { unstable_scheduleCallback as scheduleCallback, unstable_NormalPriority as NormalPriority, @@ -32,14 +35,20 @@ import { import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags'; import {isHigherEventPriority} from 'react-reconciler/src/ReactEventPriorities'; -let attemptSynchronousHydration: (fiber: Object) => void; +let _attemptSynchronousHydration: (fiber: Object) => void; export function setAttemptSynchronousHydration(fn: (fiber: Object) => void) { - attemptSynchronousHydration = fn; + _attemptSynchronousHydration = fn; } -export function attemptSynchronousEventHydration(fiber: Object): void { - attemptSynchronousHydration(fiber); +export function attemptSynchronousHydration(fiber: Object) { + _attemptSynchronousHydration(fiber); +} + +let attemptDiscreteHydration: (fiber: Object) => void; + +export function setAttemptDiscreteHydration(fn: (fiber: Object) => void) { + attemptDiscreteHydration = fn; } let attemptContinuousHydration: (fiber: Object) => void; @@ -90,6 +99,9 @@ type QueuedReplayableEvent = {| let hasScheduledReplayAttempt = false; +// The queue of discrete events to be replayed. +const queuedDiscreteEvents: Array = []; + // Indicates if any continuous event targets are non-null for early bailout. const hasAnyQueuedContinuousEvents: boolean = false; // The last of each continuous event type. We only need to replay the last one @@ -109,10 +121,49 @@ type QueuedHydrationTarget = {| |}; const queuedExplicitHydrationTargets: Array = []; +export function hasQueuedDiscreteEvents(): boolean { + return queuedDiscreteEvents.length > 0; +} + export function hasQueuedContinuousEvents(): boolean { return hasAnyQueuedContinuousEvents; } +const discreteReplayableEvents: Array = [ + 'mousedown', + 'mouseup', + 'touchcancel', + 'touchend', + 'touchstart', + 'auxclick', + 'dblclick', + 'pointercancel', + 'pointerdown', + 'pointerup', + 'dragend', + 'dragstart', + 'drop', + 'compositionend', + 'compositionstart', + 'keydown', + 'keypress', + 'keyup', + 'input', + 'textInput', // Intentionally camelCase + 'copy', + 'cut', + 'paste', + 'click', + 'change', + 'contextmenu', + 'reset', + 'submit', +]; + +export function isReplayableDiscreteEvent(eventType: DOMEventName): boolean { + return discreteReplayableEvents.indexOf(eventType) > -1; +} + function createQueuedReplayableEvent( blockedOn: null | Container | SuspenseInstance, domEventName: DOMEventName, @@ -129,6 +180,50 @@ function createQueuedReplayableEvent( }; } +export function queueDiscreteEvent( + blockedOn: null | Container | SuspenseInstance, + domEventName: DOMEventName, + eventSystemFlags: EventSystemFlags, + targetContainer: EventTarget, + nativeEvent: AnyNativeEvent, +): void { + if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) { + return; + } + const queuedEvent = createQueuedReplayableEvent( + blockedOn, + domEventName, + eventSystemFlags, + targetContainer, + nativeEvent, + ); + queuedDiscreteEvents.push(queuedEvent); + if (enableSelectiveHydration) { + if (queuedDiscreteEvents.length === 1) { + // If this was the first discrete event, we might be able to + // synchronously unblock it so that preventDefault still works. + while (queuedEvent.blockedOn !== null) { + const fiber = getInstanceFromNode(queuedEvent.blockedOn); + if (fiber === null) { + break; + } + attemptSynchronousHydration(fiber); + if (queuedEvent.blockedOn === null) { + // We got unblocked by hydration. Let's try again. + replayUnblockedEvents(); + // If we're reblocked, on an inner boundary, we might need + // to attempt hydrating that one. + continue; + } else { + // We're still blocked from hydration, we have to give up + // and replay later. + break; + } + } + } + } +} + // Resets the replaying for this type of continuous event to no event. export function clearIfContinuousEvent( domEventName: DOMEventName, @@ -398,7 +493,44 @@ function attemptReplayContinuousQueuedEventInMap( function replayUnblockedEvents() { hasScheduledReplayAttempt = false; - // Replay continuous events. + if (!enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) { + // First replay discrete events. + while (queuedDiscreteEvents.length > 0) { + const nextDiscreteEvent = queuedDiscreteEvents[0]; + if (nextDiscreteEvent.blockedOn !== null) { + // We're still blocked. + // Increase the priority of this boundary to unblock + // the next discrete event. + const fiber = getInstanceFromNode(nextDiscreteEvent.blockedOn); + if (fiber !== null) { + attemptDiscreteHydration(fiber); + } + break; + } + const targetContainers = nextDiscreteEvent.targetContainers; + while (targetContainers.length > 0) { + const targetContainer = targetContainers[0]; + const nextBlockedOn = attemptToDispatchEvent( + nextDiscreteEvent.domEventName, + nextDiscreteEvent.eventSystemFlags, + targetContainer, + nextDiscreteEvent.nativeEvent, + ); + if (nextBlockedOn !== null) { + // We're still blocked. Try again later. + nextDiscreteEvent.blockedOn = nextBlockedOn; + break; + } + // This target container was successfully dispatched. Try the next. + targetContainers.shift(); + } + if (nextDiscreteEvent.blockedOn === null) { + // We've successfully replayed the first event. Let's try the next one. + queuedDiscreteEvents.shift(); + } + } + } + // Next replay any continuous events. if (queuedFocus !== null && attemptReplayContinuousQueuedEvent(queuedFocus)) { queuedFocus = null; } @@ -431,6 +563,21 @@ function scheduleCallbackIfUnblocked( export function retryIfBlockedOn( unblocked: Container | SuspenseInstance, ): void { + // Mark anything that was blocked on this as no longer blocked + // and eligible for a replay. + if (queuedDiscreteEvents.length > 0) { + scheduleCallbackIfUnblocked(queuedDiscreteEvents[0], unblocked); + // This is a exponential search for each boundary that commits. I think it's + // worth it because we expect very few discrete events to queue up and once + // we are actually fully unblocked it will be fast to replay them. + for (let i = 1; i < queuedDiscreteEvents.length; i++) { + const queuedEvent = queuedDiscreteEvents[i]; + if (queuedEvent.blockedOn === unblocked) { + queuedEvent.blockedOn = null; + } + } + } + if (queuedFocus !== null) { scheduleCallbackIfUnblocked(queuedFocus, unblocked); } diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js index 1798a464ac288..4f2df6fffc495 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -647,13 +647,8 @@ describe('DOMPluginEventSystem', () => { await promise; }); - // We're now fully hydrated. But the click isn't replayed. - expect(clicks).toBe(0); + // We're now full hydrated. - await act(async () => { - a.click(); - }); - // Clicks can go through now expect(clicks).toBe(1); document.body.removeChild(parentContainer); diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 7dac786438236..6e992ddd7d3b1 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -26,6 +26,7 @@ import { flushPassiveEffects as flushPassiveEffects_old, getPublicRootInstance as getPublicRootInstance_old, attemptSynchronousHydration as attemptSynchronousHydration_old, + attemptDiscreteHydration as attemptDiscreteHydration_old, attemptContinuousHydration as attemptContinuousHydration_old, attemptHydrationAtCurrentPriority as attemptHydrationAtCurrentPriority_old, findHostInstance as findHostInstance_old, @@ -62,6 +63,7 @@ import { flushPassiveEffects as flushPassiveEffects_new, getPublicRootInstance as getPublicRootInstance_new, attemptSynchronousHydration as attemptSynchronousHydration_new, + attemptDiscreteHydration as attemptDiscreteHydration_new, attemptContinuousHydration as attemptContinuousHydration_new, attemptHydrationAtCurrentPriority as attemptHydrationAtCurrentPriority_new, findHostInstance as findHostInstance_new, @@ -117,6 +119,9 @@ export const getPublicRootInstance = enableNewReconciler export const attemptSynchronousHydration = enableNewReconciler ? attemptSynchronousHydration_new : attemptSynchronousHydration_old; +export const attemptDiscreteHydration = enableNewReconciler + ? attemptDiscreteHydration_new + : attemptDiscreteHydration_old; export const attemptContinuousHydration = enableNewReconciler ? attemptContinuousHydration_new : attemptContinuousHydration_old; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index db6895b002e4b..6c42165a30347 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -33,7 +33,6 @@ import { SuspenseComponent, } from './ReactWorkTags'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; -import invariant from 'shared/invariant'; import isArray from 'shared/isArray'; import {enableSchedulingProfiler} from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; @@ -153,12 +152,11 @@ function findHostInstance(component: Object): PublicInstance | null { const fiber = getInstance(component); if (fiber === undefined) { if (typeof component.render === 'function') { - invariant(false, 'Unable to find node on an unmounted component.'); + throw new Error('Unable to find node on an unmounted component.'); } else { - invariant( - false, - 'Argument appears to not be a ReactComponent. Keys: %s', - Object.keys(component), + const keys = Object.keys(component).join(','); + throw new Error( + `Argument appears to not be a ReactComponent. Keys: ${keys}`, ); } } @@ -177,12 +175,11 @@ function findHostInstanceWithWarning( const fiber = getInstance(component); if (fiber === undefined) { if (typeof component.render === 'function') { - invariant(false, 'Unable to find node on an unmounted component.'); + throw new Error('Unable to find node on an unmounted component.'); } else { - invariant( - false, - 'Argument appears to not be a ReactComponent. Keys: %s', - Object.keys(component), + const keys = Object.keys(component).join(','); + throw new Error( + `Argument appears to not be a ReactComponent. Keys: ${keys}`, ); } } @@ -390,6 +387,20 @@ function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) { } } +export function attemptDiscreteHydration(fiber: Fiber): void { + if (fiber.tag !== SuspenseComponent) { + // We ignore HostRoots here because we can't increase + // their priority and they should not suspend on I/O, + // since you have to wrap anything that might suspend in + // Suspense. + return; + } + const eventTime = requestEventTime(); + const lane = SyncLane; + scheduleUpdateOnFiber(fiber, lane, eventTime); + markRetryLaneIfNotHydrated(fiber, lane); +} + export function attemptContinuousHydration(fiber: Fiber): void { if (fiber.tag !== SuspenseComponent) { // We ignore HostRoots here because we can't increase diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 81126418f3255..7693cc7fe4006 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -33,7 +33,6 @@ import { SuspenseComponent, } from './ReactWorkTags'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; -import invariant from 'shared/invariant'; import isArray from 'shared/isArray'; import {enableSchedulingProfiler} from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; @@ -153,12 +152,11 @@ function findHostInstance(component: Object): PublicInstance | null { const fiber = getInstance(component); if (fiber === undefined) { if (typeof component.render === 'function') { - invariant(false, 'Unable to find node on an unmounted component.'); + throw new Error('Unable to find node on an unmounted component.'); } else { - invariant( - false, - 'Argument appears to not be a ReactComponent. Keys: %s', - Object.keys(component), + const keys = Object.keys(component).join(','); + throw new Error( + `Argument appears to not be a ReactComponent. Keys: ${keys}`, ); } } @@ -177,12 +175,11 @@ function findHostInstanceWithWarning( const fiber = getInstance(component); if (fiber === undefined) { if (typeof component.render === 'function') { - invariant(false, 'Unable to find node on an unmounted component.'); + throw new Error('Unable to find node on an unmounted component.'); } else { - invariant( - false, - 'Argument appears to not be a ReactComponent. Keys: %s', - Object.keys(component), + const keys = Object.keys(component).join(','); + throw new Error( + `Argument appears to not be a ReactComponent. Keys: ${keys}`, ); } } @@ -390,6 +387,20 @@ function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) { } } +export function attemptDiscreteHydration(fiber: Fiber): void { + if (fiber.tag !== SuspenseComponent) { + // We ignore HostRoots here because we can't increase + // their priority and they should not suspend on I/O, + // since you have to wrap anything that might suspend in + // Suspense. + return; + } + const eventTime = requestEventTime(); + const lane = SyncLane; + scheduleUpdateOnFiber(fiber, lane, eventTime); + markRetryLaneIfNotHydrated(fiber, lane); +} + export function attemptContinuousHydration(fiber: Fiber): void { if (fiber.tag !== SuspenseComponent) { // We ignore HostRoots here because we can't increase diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 608db694be518..f5d34e2ff6539 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -103,6 +103,8 @@ export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; + export const enableComponentStackLocations = true; export const enableNewReconciler = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 9c3d45528c5af..5a1c1ed8d9c60 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -50,6 +50,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = false; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index bf96cc00e687c..3c11070d6ecb3 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = false; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 0d2b226adf2f6..4b0457c219587 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index f760425ce03cb..2c54c1fb77c20 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -51,6 +51,7 @@ export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableStrictEffects = false; export const createRootStrictEffectsByDefault = false; export const enableUseRefAccessWarning = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index cb0efcc34bdcd..be60bcfbddb77 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index d1cfdbb749565..76af047ab6d4e 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 62a10f475acaa..6fc6cf3c68367 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = !__EXPERIMENTAL__; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 6400ef0422503..ef980529f3f21 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -27,6 +27,7 @@ export const enableLazyContextPropagation = __VARIANT__; export const enableSyncDefaultUpdates = __VARIANT__; export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__; export const warnOnSubscriptionInsideStartTransition = __VARIANT__; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__; // Enable this flag to help with concurrent mode debugging. // It logs information to the console about React scheduling, rendering, and commit phases. diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index c5dde53578c95..0f1ce6eb1f9f7 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -32,6 +32,7 @@ export const { enableLazyContextPropagation, enableSyncDefaultUpdates, warnOnSubscriptionInsideStartTransition, + enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build. From 1bb05371760e3880b83becbbdbcd105e0a416ed8 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Fri, 1 Oct 2021 11:34:14 -0400 Subject: [PATCH 08/26] lint --- .../ReactDOMServerPartialHydration-test.internal.js | 2 +- ...ReactDOMServerSelectiveHydration-test.internal.js | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index ec54c21f73c1f..d469d30e89204 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -2722,7 +2722,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - const root = ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, ); Scheduler.unstable_flushAll(); jest.runAllTimers(); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 2e5e55a07bd26..d49eef9b539e7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -984,10 +984,12 @@ describe('ReactDOMServerSelectiveHydration', () => { Scheduler.unstable_yieldValue(text); const ref = React.useRef(); React.useLayoutEffect(() => { - ref.current && - ref.current.addEventListener('click', () => { - Scheduler.unstable_yieldValue('Native Click ' + text); - }); + if (!ref.current) { + return; + } + ref.current.addEventListener('click', () => { + Scheduler.unstable_yieldValue('Native Click ' + text); + }); }, [text]); return ( { const span = container.getElementsByTagName('span')[1]; - const root = ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, ); // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); From a5e8a2b4ad0ba353751c778c23c0411321425860 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Fri, 1 Oct 2021 11:49:58 -0400 Subject: [PATCH 09/26] fix test --- ...MServerSelectiveHydration-test.internal.js | 22 +++++++++++-------- .../DOMPluginEventSystem-test.internal.js | 8 ++++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index d49eef9b539e7..b6f48b311d039 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -980,6 +980,7 @@ describe('ReactDOMServerSelectiveHydration', () => { // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('Fires capture event handlers and native events if content is hydratable during discrete event', async () => { + spyOnDev(console, 'error'); function Child({text}) { Scheduler.unstable_yieldValue(text); const ref = React.useRef(); @@ -1019,15 +1020,16 @@ describe('ReactDOMServerSelectiveHydration', () => { ); } - spyOnDev(console, 'error'); const finalHTML = ReactDOMServer.renderToString(); - expect(console.error).toHaveBeenCalledTimes(2); - expect(console.error.calls.argsFor(0)[0]).toContain( - 'useLayoutEffect does nothing on the server', - ); - expect(console.error.calls.argsFor(1)[0]).toContain( - 'useLayoutEffect does nothing on the server', - ); + if (__DEV__) { + expect(console.error).toHaveBeenCalledTimes(2); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'useLayoutEffect does nothing on the server', + ); + expect(console.error.calls.argsFor(1)[0]).toContain( + 'useLayoutEffect does nothing on the server', + ); + } expect(Scheduler).toHaveYielded(['App', 'A', 'B']); @@ -1061,6 +1063,8 @@ describe('ReactDOMServerSelectiveHydration', () => { expect(Scheduler).toFlushAndYield(['A']); document.body.removeChild(container); - expect(console.error).toHaveBeenCalledTimes(2); + if (__DEV__) { + expect(console.error).toHaveBeenCalledTimes(2); + } }); }); diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js index 4f2df6fffc495..a7ec31781603e 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -649,7 +649,13 @@ describe('DOMPluginEventSystem', () => { // We're now full hydrated. - expect(clicks).toBe(1); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(clicks).toBe(0); + } else { + expect(clicks).toBe(1); + } document.body.removeChild(parentContainer); }); From f92b36d3db3b5088c6040632e9568fbe02faabc2 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Fri, 1 Oct 2021 12:07:34 -0400 Subject: [PATCH 10/26] only gate small parts of some tests --- ...DOMServerPartialHydration-test.internal.js | 79 ++++++++++++++----- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index d469d30e89204..2368acc355ee8 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1831,7 +1831,6 @@ describe('ReactDOMServerPartialHydration', () => { expect(newSpan.className).toBe('hi'); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a hydrated node until it commits', async () => { let suspend = false; let resolve; @@ -1906,15 +1905,20 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(clicks).toBe(1); - - expect(container.textContent).toBe('Hello'); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(clicks).toBe(0); + expect(container.textContent).toBe('Click meHello'); + } else { + expect(clicks).toBe(1); + expect(container.textContent).toBe('Hello'); + } document.body.removeChild(container); }); // @gate www - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a hydrated event handle until it commits', async () => { const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; @@ -1993,12 +1997,17 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - expect(onEvent).toHaveBeenCalledTimes(2); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(onEvent).toHaveBeenCalledTimes(0); + } else { + expect(onEvent).toHaveBeenCalledTimes(2); + } document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('invokes discrete events on nested suspense boundaries in a root (legacy system)', async () => { let suspend = false; let resolve; @@ -2075,13 +2084,19 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(clicks).toBe(2); + + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(clicks).toBe(0); + } else { + expect(clicks).toBe(2); + } document.body.removeChild(container); }); // @gate www - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('invokes discrete events on nested suspense boundaries in a root (createEventHandle)', async () => { let suspend = false; let isServerRendering = true; @@ -2162,12 +2177,17 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(onEvent).toHaveBeenCalledTimes(2); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(onEvent).toHaveBeenCalledTimes(0); + } else { + expect(onEvent).toHaveBeenCalledTimes(2); + } document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke the parent of dehydrated boundary event', async () => { let suspend = false; let resolve; @@ -2236,14 +2256,20 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - expect(clicksOnChild).toBe(1); - // This will be zero due to the stopPropagation. - expect(clicksOnParent).toBe(0); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(clicksOnChild).toBe(0); + expect(clicksOnParent).toBe(0); + } else { + expect(clicksOnChild).toBe(1); + // This will be zero due to the stopPropagation. + expect(clicksOnParent).toBe(0); + } document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => { let suspend = false; let resolve; @@ -2316,8 +2342,13 @@ describe('ReactDOMServerPartialHydration', () => { }); // We're now full hydrated. - - expect(clicks).toBe(1); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(clicks).toBe(0); + } else { + expect(clicks).toBe(1); + } document.body.removeChild(parentContainer); }); @@ -2508,7 +2539,6 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).not.toBe(null); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('regression test: does not overfire non-bubbling browser events', async () => { let suspend = false; let resolve; @@ -2588,8 +2618,17 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - expect(submits).toBe(1); - expect(container.textContent).toBe('Hello'); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + // discrete event not replayed + expect(submits).toBe(0); + expect(container.textContent).toBe('Click meHello'); + } else { + expect(submits).toBe(1); + expect(container.textContent).toBe('Hello'); + } + document.body.removeChild(container); }); From 714509b20f3f3ed8e4eb9601d0715f24a9f31d87 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Fri, 1 Oct 2021 16:01:06 -0400 Subject: [PATCH 11/26] use gate function in tests instead --- ...DOMServerPartialHydration-test.internal.js | 35 +++++++++++++++---- .../DOMPluginEventSystem-test.internal.js | 5 ++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 2368acc355ee8..5bc17c9bc0a30 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1907,7 +1907,10 @@ describe('ReactDOMServerPartialHydration', () => { }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(clicks).toBe(0); expect(container.textContent).toBe('Click meHello'); @@ -1998,7 +2001,10 @@ describe('ReactDOMServerPartialHydration', () => { }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(onEvent).toHaveBeenCalledTimes(0); } else { @@ -2086,7 +2092,10 @@ describe('ReactDOMServerPartialHydration', () => { }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(clicks).toBe(0); } else { @@ -2178,7 +2187,10 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(onEvent).toHaveBeenCalledTimes(0); } else { @@ -2257,7 +2269,10 @@ describe('ReactDOMServerPartialHydration', () => { }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(clicksOnChild).toBe(0); expect(clicksOnParent).toBe(0); @@ -2343,7 +2358,10 @@ describe('ReactDOMServerPartialHydration', () => { // We're now full hydrated. if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(clicks).toBe(0); } else { @@ -2619,7 +2637,10 @@ describe('ReactDOMServerPartialHydration', () => { }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { // discrete event not replayed expect(submits).toBe(0); diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js index a7ec31781603e..1b2bfbb0bef4d 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -650,7 +650,10 @@ describe('DOMPluginEventSystem', () => { // We're now full hydrated. if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(clicks).toBe(0); } else { From 351c238f3490c5f31ab4419520b54214b67602a6 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 19:45:03 -0400 Subject: [PATCH 12/26] synchronously hydrate discrete events in capture phase and dont replay them --- ...DOMServerPartialHydration-test.internal.js | 80 ++++++++-- ...MServerSelectiveHydration-test.internal.js | 93 ++++++------ .../src/events/ReactDOMEventListener.js | 80 +++++----- .../src/events/ReactDOMEventReplaying.js | 139 +----------------- .../DOMPluginEventSystem-test.internal.js | 7 +- 5 files changed, 159 insertions(+), 240 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 9f954f4581d3c..5a34c2ca89ebf 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1905,8 +1905,15 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(clicks).toBe(1); + // Clicks aren't replayed + expect(clicks).toBe(0); + expect(container.textContent).toBe('Click meHello'); + await act(async () => { + a.click(); + }); + // Now click went through + expect(clicks).toBe(1); expect(container.textContent).toBe('Hello'); document.body.removeChild(container); @@ -1991,8 +1998,13 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - expect(onEvent).toHaveBeenCalledTimes(2); + // Clicks are not replayed + expect(onEvent).toHaveBeenCalledTimes(0); + await act(async () => { + a.click(); + }); + expect(onEvent).toHaveBeenCalledTimes(1); document.body.removeChild(container); }); @@ -2072,7 +2084,13 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(clicks).toBe(2); + // clicks are not replayed + expect(clicks).toBe(0); + + await act(async () => { + a.click(); + }); + expect(clicks).toBe(1); document.body.removeChild(container); }); @@ -2158,7 +2176,16 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(onEvent).toHaveBeenCalledTimes(2); + + // Stays 0 because we don't replay clicks + expect(onEvent).toHaveBeenCalledTimes(0); + + await act(async () => { + a.click(); + }); + + // Clicks now go through after resolving + expect(onEvent).toHaveBeenCalledTimes(1); document.body.removeChild(container); }); @@ -2231,6 +2258,13 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); + // Stays 0 because we don't replay click events + expect(clicksOnChild).toBe(0); + + await act(async () => { + span.click(); + }); + // Now click events go through expect(clicksOnChild).toBe(1); // This will be zero due to the stopPropagation. expect(clicksOnParent).toBe(0); @@ -2309,8 +2343,12 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - // We're now full hydrated. + // We're now full hydrated but we don't replay clicks + expect(clicks).toBe(0); + await act(async () => { + a.click(); + }); expect(clicks).toBe(1); document.body.removeChild(parentContainer); @@ -2351,7 +2389,7 @@ describe('ReactDOMServerPartialHydration', () => { onMouseLeave={() => ops.push('Mouse Leave First')} /> {/* We suspend after to test what happens when we eager - attach the listener. */} + attach the listener. */}
@@ -2396,9 +2434,11 @@ describe('ReactDOMServerPartialHydration', () => { expect(ops).toEqual([]); // Resolving the second promise so that rendering can complete. - suspend2 = false; - resolve2(); - await promise2; + await act(async () => { + suspend2 = false; + resolve2(); + await promise2; + }); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -2407,10 +2447,12 @@ describe('ReactDOMServerPartialHydration', () => { // able to replay it now. expect(ops).toEqual(['Mouse Enter Second']); - // Resolving the first promise has no effect now. - suspend1 = false; - resolve1(); - await promise1; + await act(async () => { + // Resolving the first promise has no effect now. + suspend1 = false; + resolve1(); + await promise1; + }); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -2580,6 +2622,18 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); + // Submit event is not replayed + expect(submits).toBe(0); + expect(container.textContent).toBe('Click meHello'); + + await act(async () => { + form.dispatchEvent( + new Event('submit', { + bubbles: true, + }), + ); + }); + // Submit event has now gone through expect(submits).toBe(1); expect(container.textContent).toBe('Hello'); document.body.removeChild(container); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 76d9b43d07f61..111614e9cd422 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -129,8 +129,10 @@ describe('ReactDOMServerSelectiveHydration', () => { Scheduler.unstable_yieldValue(text); return ( { + Scheduler.unstable_yieldValue('Capture Clicked ' + text); + }} onClick={e => { - e.preventDefault(); Scheduler.unstable_yieldValue('Clicked ' + text); }}> {text} @@ -172,13 +174,15 @@ describe('ReactDOMServerSelectiveHydration', () => { // This should synchronously hydrate the root App and the second suspense // boundary. - const result = dispatchClickEvent(span); - - // The event should have been canceled because we called preventDefault. - expect(result).toBe(false); + dispatchClickEvent(span); // We rendered App, B and then invoked the event without rendering A. - expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']); + expect(Scheduler).toHaveYielded([ + 'App', + 'B', + 'Capture Clicked B', + 'Clicked B', + ]); // After continuing the scheduler, we finally hydrate A. expect(Scheduler).toFlushAndYield(['A']); @@ -256,7 +260,7 @@ describe('ReactDOMServerSelectiveHydration', () => { }); expect(Scheduler).toHaveYielded([ 'App', - // Continuing rendering will render B next. + // B and C don't suspense so they are rendered immediately 'B', 'C', ]); @@ -266,9 +270,9 @@ describe('ReactDOMServerSelectiveHydration', () => { resolve(); await promise; }); - // After the click, we should prioritize D and the Click first, + // After the click, we should prioritize hydrating D // and only after that render A and C. - expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); + expect(Scheduler).toHaveYielded(['D', 'A']); document.body.removeChild(container); }); @@ -338,12 +342,13 @@ describe('ReactDOMServerSelectiveHydration', () => { // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); - // This click target cannot be hydrated yet because the first is Suspended. + // A and D cannot be hydrated yet because they Suspended. dispatchClickEvent(spanA); - dispatchClickEvent(spanC); dispatchClickEvent(spanD); - expect(Scheduler).toHaveYielded(['App']); + // C can be immediately hydrated in capture phase in time for it to be clicked + dispatchClickEvent(spanC); + expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']); await act(async () => { suspend = false; @@ -351,15 +356,11 @@ describe('ReactDOMServerSelectiveHydration', () => { await promise; }); - // We should prioritize hydrating A, C and D first since we clicked in + // We should prioritize hydrating A then D first since we clicked in // them. Only after they're done will we hydrate B. expect(Scheduler).toHaveYielded([ 'A', - 'Clicked A', - 'C', - 'Clicked C', 'D', - 'Clicked D', // B should render last since it wasn't clicked. 'B', ]); @@ -512,14 +513,15 @@ describe('ReactDOMServerSelectiveHydration', () => { }); expect(Scheduler).toHaveYielded(['App', 'B', 'C']); - // After the click, we should prioritize D and the Click first, - // and only after that render A and C. + // After the click, we should prioritize D, + // and only after that render A await act(async () => { suspend = false; resolve(); await promise; }); - expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); + // No click is yielded since we don't replay clicks + expect(Scheduler).toHaveYielded(['D', 'A']); document.body.removeChild(container); }); @@ -596,10 +598,12 @@ describe('ReactDOMServerSelectiveHydration', () => { // This click target cannot be hydrated yet because the first is Suspended. createEventTarget(spanA).virtualclick(); - createEventTarget(spanC).virtualclick(); createEventTarget(spanD).virtualclick(); - expect(Scheduler).toHaveYielded(['App']); + // C can be immediately hydrated in capture phase in time for click + createEventTarget(spanC).virtualclick(); + + expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']); await act(async () => { suspend = false; @@ -611,11 +615,7 @@ describe('ReactDOMServerSelectiveHydration', () => { // them. Only after they're done will we hydrate B. expect(Scheduler).toHaveYielded([ 'A', - 'Clicked A', - 'C', - 'Clicked C', 'D', - 'Clicked D', // B should render last since it wasn't clicked. 'B', ]); @@ -699,7 +699,8 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchMouseHoverEvent(spanB, spanD); dispatchMouseHoverEvent(spanC, spanB); - expect(Scheduler).toHaveYielded(['App']); + // We can hydrate B and C now. + expect(Scheduler).toHaveYielded(['App', 'B', 'C']); await act(async () => { suspend = false; @@ -713,14 +714,7 @@ describe('ReactDOMServerSelectiveHydration', () => { // the same time since B was already scheduled. // This is ok because it will at least not continue for nested // boundary. See the next test below. - expect(Scheduler).toHaveYielded([ - 'D', - 'Clicked D', - 'B', // Ideally this should be later. - 'C', - 'Hover C', - 'A', - ]); + expect(Scheduler).toHaveYielded(['D', 'A']); document.body.removeChild(container); }); @@ -796,16 +790,22 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchMouseHoverEvent(spanB, spanD); dispatchMouseHoverEvent(spanC, spanB); - suspend = false; - resolve(); - await promise; + await act(async () => { + suspend = false; + resolve(); + await promise; + }); + + // C renders before B since its the current hover target + // B renders because its priority was increased when it was hovered over + expect(Scheduler).toHaveYielded(['App', 'C', 'B', 'Hover C', 'D', 'A']); // We should prioritize hydrating D first because we clicked it. // Next we should hydrate C since that's the current hover target. // Next it doesn't matter if we hydrate A or B first but as an // implementation detail we're currently hydrating B first since // we at one point hovered over it and we never deprioritized it. - expect(Scheduler).toFlushAndYield(['App', 'C', 'Hover C', 'A', 'B', 'D']); + expect(Scheduler).toFlushAndYield([]); document.body.removeChild(container); }); @@ -942,20 +942,17 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchClickEvent(spanC); expect(Scheduler).toHaveYielded([ - // Hydrate C first since we clicked it. + // Hydrate A and B since we hovered + // then Hydrate C since we clicked it. + 'A', + 'a', + 'B', + 'b', 'C', 'c', ]); expect(Scheduler).toFlushAndYield([ - // Finish hydration of A since we forced it to hydrate. - 'A', - 'a', - // Also, hydrate B since we hovered over it. - // It's not important which one comes first. A or B. - // As long as they both happen before the Idle update. - 'B', - 'b', // Begin the Idle update again. 'App', 'AA', diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index dfe3c2e3c00c0..1f9794c9817f5 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -13,12 +13,11 @@ import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig'; import type {DOMEventName} from '../events/DOMEventNames'; import { - isReplayableDiscreteEvent, - queueDiscreteEvent, - hasQueuedDiscreteEvents, + attemptSynchronousEventHydration, clearIfContinuousEvent, queueIfContinuousEvent, } from './ReactDOMEventReplaying'; +import { enableSelectiveHydration } from 'shared/ReactFeatureFlags'; import { getNearestMountedFiber, getContainerFromFiber, @@ -28,7 +27,10 @@ import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags'; import {type EventSystemFlags, IS_CAPTURE_PHASE} from './EventSystemFlags'; import getEventTarget from './getEventTarget'; -import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; +import { + getInstanceFromNode, + getClosestInstanceFromNode, +} from '../client/ReactDOMComponentTree'; import {dispatchEventForPluginEventSystem} from './DOMPluginEventSystem'; @@ -158,25 +160,7 @@ export function dispatchEvent( // to filter them out until we fix the logic to handle them correctly. const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0; - if ( - allowReplay && - hasQueuedDiscreteEvents() && - isReplayableDiscreteEvent(domEventName) - ) { - // If we already have a queue of discrete events, and this is another discrete - // event, then we can't dispatch it regardless of its target, since they - // need to dispatch in order. - queueDiscreteEvent( - null, // Flags that we're not actually blocked on anything as far as we know. - domEventName, - eventSystemFlags, - targetContainer, - nativeEvent, - ); - return; - } - - const blockedOn = attemptToDispatchEvent( + let blockedOn = attemptToDispatchEvent( domEventName, eventSystemFlags, targetContainer, @@ -184,40 +168,48 @@ export function dispatchEvent( ); if (blockedOn === null) { - // We successfully dispatched this event. if (allowReplay) { + // We successfully dispatched this event. clearIfContinuousEvent(domEventName, nativeEvent); } return; } - if (allowReplay) { - if (isReplayableDiscreteEvent(domEventName)) { - // This this to be replayed later once the target is available. - queueDiscreteEvent( - blockedOn, + if ( + allowReplay && + queueIfContinuousEvent( + blockedOn, + domEventName, + eventSystemFlags, + targetContainer, + nativeEvent, + ) + ) { + return; + } + + // Synchronously hydrate non-replayable / non-continuous events + if (enableSelectiveHydration) { + while (blockedOn !== null) { + const fiber = getInstanceFromNode(blockedOn); + if (fiber !== null) { + attemptSynchronousEventHydration(fiber); + } + const nextBlockedOn = attemptToDispatchEvent( domEventName, eventSystemFlags, targetContainer, nativeEvent, ); - return; - } - if ( - queueIfContinuousEvent( - blockedOn, - domEventName, - eventSystemFlags, - targetContainer, - nativeEvent, - ) - ) { - return; + if (nextBlockedOn === blockedOn) { + break; + } + blockedOn = nextBlockedOn; } - // We need to clear only if we didn't queue because - // queueing is accumulative. - clearIfContinuousEvent(domEventName, nativeEvent); } + // We need to clear only if we didn't queue because + // queueing is accumulative. + clearIfContinuousEvent(domEventName, nativeEvent); // This is not replayable so we'll invoke it but without a target, // in case the event system needs to trace it. diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index eab9b4650c108..f3a7eca1ba88c 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -44,6 +44,10 @@ export function setAttemptDiscreteHydration(fn: (fiber: Object) => void) { attemptDiscreteHydration = fn; } +export function attemptSynchronousEventHydration(fiber: Object): void { + attemptSynchronousHydration(fiber); +} + let attemptContinuousHydration: (fiber: Object) => void; export function setAttemptContinuousHydration(fn: (fiber: Object) => void) { @@ -92,9 +96,6 @@ type QueuedReplayableEvent = {| let hasScheduledReplayAttempt = false; -// The queue of discrete events to be replayed. -const queuedDiscreteEvents: Array = []; - // Indicates if any continuous event targets are non-null for early bailout. const hasAnyQueuedContinuousEvents: boolean = false; // The last of each continuous event type. We only need to replay the last one @@ -114,49 +115,10 @@ type QueuedHydrationTarget = {| |}; const queuedExplicitHydrationTargets: Array = []; -export function hasQueuedDiscreteEvents(): boolean { - return queuedDiscreteEvents.length > 0; -} - export function hasQueuedContinuousEvents(): boolean { return hasAnyQueuedContinuousEvents; } -const discreteReplayableEvents: Array = [ - 'mousedown', - 'mouseup', - 'touchcancel', - 'touchend', - 'touchstart', - 'auxclick', - 'dblclick', - 'pointercancel', - 'pointerdown', - 'pointerup', - 'dragend', - 'dragstart', - 'drop', - 'compositionend', - 'compositionstart', - 'keydown', - 'keypress', - 'keyup', - 'input', - 'textInput', // Intentionally camelCase - 'copy', - 'cut', - 'paste', - 'click', - 'change', - 'contextmenu', - 'reset', - 'submit', -]; - -export function isReplayableDiscreteEvent(eventType: DOMEventName): boolean { - return discreteReplayableEvents.indexOf(eventType) > -1; -} - function createQueuedReplayableEvent( blockedOn: null | Container | SuspenseInstance, domEventName: DOMEventName, @@ -173,47 +135,6 @@ function createQueuedReplayableEvent( }; } -export function queueDiscreteEvent( - blockedOn: null | Container | SuspenseInstance, - domEventName: DOMEventName, - eventSystemFlags: EventSystemFlags, - targetContainer: EventTarget, - nativeEvent: AnyNativeEvent, -): void { - const queuedEvent = createQueuedReplayableEvent( - blockedOn, - domEventName, - eventSystemFlags, - targetContainer, - nativeEvent, - ); - queuedDiscreteEvents.push(queuedEvent); - if (enableSelectiveHydration) { - if (queuedDiscreteEvents.length === 1) { - // If this was the first discrete event, we might be able to - // synchronously unblock it so that preventDefault still works. - while (queuedEvent.blockedOn !== null) { - const fiber = getInstanceFromNode(queuedEvent.blockedOn); - if (fiber === null) { - break; - } - attemptSynchronousHydration(fiber); - if (queuedEvent.blockedOn === null) { - // We got unblocked by hydration. Let's try again. - replayUnblockedEvents(); - // If we're reblocked, on an inner boundary, we might need - // to attempt hydrating that one. - continue; - } else { - // We're still blocked from hydration, we have to give up - // and replay later. - break; - } - } - } - } -} - // Resets the replaying for this type of continuous event to no event. export function clearIfContinuousEvent( domEventName: DOMEventName, @@ -483,42 +404,7 @@ function attemptReplayContinuousQueuedEventInMap( function replayUnblockedEvents() { hasScheduledReplayAttempt = false; - // First replay discrete events. - while (queuedDiscreteEvents.length > 0) { - const nextDiscreteEvent = queuedDiscreteEvents[0]; - if (nextDiscreteEvent.blockedOn !== null) { - // We're still blocked. - // Increase the priority of this boundary to unblock - // the next discrete event. - const fiber = getInstanceFromNode(nextDiscreteEvent.blockedOn); - if (fiber !== null) { - attemptDiscreteHydration(fiber); - } - break; - } - const targetContainers = nextDiscreteEvent.targetContainers; - while (targetContainers.length > 0) { - const targetContainer = targetContainers[0]; - const nextBlockedOn = attemptToDispatchEvent( - nextDiscreteEvent.domEventName, - nextDiscreteEvent.eventSystemFlags, - targetContainer, - nextDiscreteEvent.nativeEvent, - ); - if (nextBlockedOn !== null) { - // We're still blocked. Try again later. - nextDiscreteEvent.blockedOn = nextBlockedOn; - break; - } - // This target container was successfully dispatched. Try the next. - targetContainers.shift(); - } - if (nextDiscreteEvent.blockedOn === null) { - // We've successfully replayed the first event. Let's try the next one. - queuedDiscreteEvents.shift(); - } - } - // Next replay any continuous events. + // Replay continuous events. if (queuedFocus !== null && attemptReplayContinuousQueuedEvent(queuedFocus)) { queuedFocus = null; } @@ -551,21 +437,6 @@ function scheduleCallbackIfUnblocked( export function retryIfBlockedOn( unblocked: Container | SuspenseInstance, ): void { - // Mark anything that was blocked on this as no longer blocked - // and eligible for a replay. - if (queuedDiscreteEvents.length > 0) { - scheduleCallbackIfUnblocked(queuedDiscreteEvents[0], unblocked); - // This is a exponential search for each boundary that commits. I think it's - // worth it because we expect very few discrete events to queue up and once - // we are actually fully unblocked it will be fast to replay them. - for (let i = 1; i < queuedDiscreteEvents.length; i++) { - const queuedEvent = queuedDiscreteEvents[i]; - if (queuedEvent.blockedOn === unblocked) { - queuedEvent.blockedOn = null; - } - } - } - if (queuedFocus !== null) { scheduleCallbackIfUnblocked(queuedFocus, unblocked); } diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js index 4f2df6fffc495..1798a464ac288 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -647,8 +647,13 @@ describe('DOMPluginEventSystem', () => { await promise; }); - // We're now full hydrated. + // We're now fully hydrated. But the click isn't replayed. + expect(clicks).toBe(0); + await act(async () => { + a.click(); + }); + // Clicks can go through now expect(clicks).toBe(1); document.body.removeChild(parentContainer); From eec367af5450b7efdf55904dae8b4ce335266637 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 19:54:49 -0400 Subject: [PATCH 13/26] test?? --- packages/react-dom/src/client/ReactDOM.js | 3 -- .../src/events/ReactDOMEventListener.js | 2 +- .../src/events/ReactDOMEventReplaying.js | 6 ---- .../src/ReactFiberReconciler.js | 5 --- .../src/ReactFiberReconciler.new.js | 14 -------- .../src/ReactFiberReconciler.old.js | 36 ++++++------------- 6 files changed, 12 insertions(+), 54 deletions(-) diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 8e37995cb7364..e376e9f85010a 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -28,7 +28,6 @@ import { flushControlled, injectIntoDevTools, attemptSynchronousHydration, - attemptDiscreteHydration, attemptContinuousHydration, attemptHydrationAtCurrentPriority, } from 'react-reconciler/src/ReactFiberReconciler'; @@ -53,7 +52,6 @@ import { import {restoreControlledState} from './ReactDOMComponent'; import { setAttemptSynchronousHydration, - setAttemptDiscreteHydration, setAttemptContinuousHydration, setAttemptHydrationAtCurrentPriority, queueExplicitHydrationTarget, @@ -68,7 +66,6 @@ import { } from '../events/ReactDOMControlledComponent'; setAttemptSynchronousHydration(attemptSynchronousHydration); -setAttemptDiscreteHydration(attemptDiscreteHydration); setAttemptContinuousHydration(attemptContinuousHydration); setAttemptHydrationAtCurrentPriority(attemptHydrationAtCurrentPriority); setGetCurrentUpdatePriority(getCurrentUpdatePriority); diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 1f9794c9817f5..8850be4f7bed6 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -17,7 +17,7 @@ import { clearIfContinuousEvent, queueIfContinuousEvent, } from './ReactDOMEventReplaying'; -import { enableSelectiveHydration } from 'shared/ReactFeatureFlags'; +import {enableSelectiveHydration} from 'shared/ReactFeatureFlags'; import { getNearestMountedFiber, getContainerFromFiber, diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index f3a7eca1ba88c..6a29f3fa777d1 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -38,12 +38,6 @@ export function setAttemptSynchronousHydration(fn: (fiber: Object) => void) { attemptSynchronousHydration = fn; } -let attemptDiscreteHydration: (fiber: Object) => void; - -export function setAttemptDiscreteHydration(fn: (fiber: Object) => void) { - attemptDiscreteHydration = fn; -} - export function attemptSynchronousEventHydration(fiber: Object): void { attemptSynchronousHydration(fiber); } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 6e992ddd7d3b1..7dac786438236 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -26,7 +26,6 @@ import { flushPassiveEffects as flushPassiveEffects_old, getPublicRootInstance as getPublicRootInstance_old, attemptSynchronousHydration as attemptSynchronousHydration_old, - attemptDiscreteHydration as attemptDiscreteHydration_old, attemptContinuousHydration as attemptContinuousHydration_old, attemptHydrationAtCurrentPriority as attemptHydrationAtCurrentPriority_old, findHostInstance as findHostInstance_old, @@ -63,7 +62,6 @@ import { flushPassiveEffects as flushPassiveEffects_new, getPublicRootInstance as getPublicRootInstance_new, attemptSynchronousHydration as attemptSynchronousHydration_new, - attemptDiscreteHydration as attemptDiscreteHydration_new, attemptContinuousHydration as attemptContinuousHydration_new, attemptHydrationAtCurrentPriority as attemptHydrationAtCurrentPriority_new, findHostInstance as findHostInstance_new, @@ -119,9 +117,6 @@ export const getPublicRootInstance = enableNewReconciler export const attemptSynchronousHydration = enableNewReconciler ? attemptSynchronousHydration_new : attemptSynchronousHydration_old; -export const attemptDiscreteHydration = enableNewReconciler - ? attemptDiscreteHydration_new - : attemptDiscreteHydration_old; export const attemptContinuousHydration = enableNewReconciler ? attemptContinuousHydration_new : attemptContinuousHydration_old; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index 6c42165a30347..e22b4a1c557c9 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -387,20 +387,6 @@ function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) { } } -export function attemptDiscreteHydration(fiber: Fiber): void { - if (fiber.tag !== SuspenseComponent) { - // We ignore HostRoots here because we can't increase - // their priority and they should not suspend on I/O, - // since you have to wrap anything that might suspend in - // Suspense. - return; - } - const eventTime = requestEventTime(); - const lane = SyncLane; - scheduleUpdateOnFiber(fiber, lane, eventTime); - markRetryLaneIfNotHydrated(fiber, lane); -} - export function attemptContinuousHydration(fiber: Fiber): void { if (fiber.tag !== SuspenseComponent) { // We ignore HostRoots here because we can't increase diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 7693cc7fe4006..e22b4a1c557c9 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -18,8 +18,8 @@ import type { } from './ReactFiberHostConfig'; import type {RendererInspectionConfig} from './ReactFiberHostConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; -import type {Lane} from './ReactFiberLane.old'; -import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; +import type {Lane} from './ReactFiberLane.new'; +import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import { findCurrentHostFiber, @@ -42,9 +42,9 @@ import { processChildContext, emptyContextObject, isContextProvider as isLegacyContextProvider, -} from './ReactFiberContext.old'; -import {createFiberRoot} from './ReactFiberRoot.old'; -import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.old'; +} from './ReactFiberContext.new'; +import {createFiberRoot} from './ReactFiberRoot.new'; +import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.new'; import { requestEventTime, requestUpdateLane, @@ -57,12 +57,12 @@ import { deferredUpdates, discreteUpdates, flushPassiveEffects, -} from './ReactFiberWorkLoop.old'; +} from './ReactFiberWorkLoop.new'; import { createUpdate, enqueueUpdate, entangleTransitions, -} from './ReactUpdateQueue.old'; +} from './ReactUpdateQueue.new'; import { isRendering as ReactCurrentFiberIsRendering, current as ReactCurrentFiberCurrent, @@ -76,20 +76,20 @@ import { NoTimestamp, getHighestPriorityPendingLanes, higherPriorityLane, -} from './ReactFiberLane.old'; +} from './ReactFiberLane.new'; import { getCurrentUpdatePriority, runWithPriority, -} from './ReactEventPriorities.old'; +} from './ReactEventPriorities.new'; import { scheduleRefresh, scheduleRoot, setRefreshHandler, findHostInstancesForRefresh, -} from './ReactFiberHotReloading.old'; +} from './ReactFiberHotReloading.new'; import {markRenderScheduled} from './SchedulingProfiler'; import ReactVersion from 'shared/ReactVersion'; -export {registerMutableSourceForHydration} from './ReactMutableSource.old'; +export {registerMutableSourceForHydration} from './ReactMutableSource.new'; export {createPortal} from './ReactPortal'; export { createComponentSelector, @@ -387,20 +387,6 @@ function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) { } } -export function attemptDiscreteHydration(fiber: Fiber): void { - if (fiber.tag !== SuspenseComponent) { - // We ignore HostRoots here because we can't increase - // their priority and they should not suspend on I/O, - // since you have to wrap anything that might suspend in - // Suspense. - return; - } - const eventTime = requestEventTime(); - const lane = SyncLane; - scheduleUpdateOnFiber(fiber, lane, eventTime); - markRetryLaneIfNotHydrated(fiber, lane); -} - export function attemptContinuousHydration(fiber: Fiber): void { if (fiber.tag !== SuspenseComponent) { // We ignore HostRoots here because we can't increase From fa545297f8c088c99478fc2731fa2c8c15c18880 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 19:56:15 -0400 Subject: [PATCH 14/26] replace old with new --- .../src/ReactFiberReconciler.old.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index e22b4a1c557c9..6970e162ee46d 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -18,8 +18,8 @@ import type { } from './ReactFiberHostConfig'; import type {RendererInspectionConfig} from './ReactFiberHostConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; -import type {Lane} from './ReactFiberLane.new'; -import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; +import type { Lane } from './ReactFiberLane.old'; +import type { SuspenseState } from './ReactFiberSuspenseComponent.old'; import { findCurrentHostFiber, @@ -42,9 +42,9 @@ import { processChildContext, emptyContextObject, isContextProvider as isLegacyContextProvider, -} from './ReactFiberContext.new'; -import {createFiberRoot} from './ReactFiberRoot.new'; -import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.new'; +} from './ReactFiberContext.old'; +import { createFiberRoot } from './ReactFiberRoot.old'; +import { injectInternals, onScheduleRoot } from './ReactFiberDevToolsHook.old'; import { requestEventTime, requestUpdateLane, @@ -57,12 +57,12 @@ import { deferredUpdates, discreteUpdates, flushPassiveEffects, -} from './ReactFiberWorkLoop.new'; +} from './ReactFiberWorkLoop.old'; import { createUpdate, enqueueUpdate, entangleTransitions, -} from './ReactUpdateQueue.new'; +} from './ReactUpdateQueue.old'; import { isRendering as ReactCurrentFiberIsRendering, current as ReactCurrentFiberCurrent, @@ -76,20 +76,20 @@ import { NoTimestamp, getHighestPriorityPendingLanes, higherPriorityLane, -} from './ReactFiberLane.new'; +} from './ReactFiberLane.old'; import { getCurrentUpdatePriority, runWithPriority, -} from './ReactEventPriorities.new'; +} from './ReactEventPriorities.old'; import { scheduleRefresh, scheduleRoot, setRefreshHandler, findHostInstancesForRefresh, -} from './ReactFiberHotReloading.new'; +} from './ReactFiberHotReloading.old'; import {markRenderScheduled} from './SchedulingProfiler'; import ReactVersion from 'shared/ReactVersion'; -export {registerMutableSourceForHydration} from './ReactMutableSource.new'; +export { registerMutableSourceForHydration } from './ReactMutableSource.old'; export {createPortal} from './ReactPortal'; export { createComponentSelector, From 567d8dd9b63c5c16753d852a751e59247f4bc999 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 19:57:54 -0400 Subject: [PATCH 15/26] prettier --- .../react-reconciler/src/ReactFiberReconciler.old.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 6970e162ee46d..badd11359ec0b 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -18,8 +18,8 @@ import type { } from './ReactFiberHostConfig'; import type {RendererInspectionConfig} from './ReactFiberHostConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; -import type { Lane } from './ReactFiberLane.old'; -import type { SuspenseState } from './ReactFiberSuspenseComponent.old'; +import type {Lane} from './ReactFiberLane.old'; +import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; import { findCurrentHostFiber, @@ -43,8 +43,8 @@ import { emptyContextObject, isContextProvider as isLegacyContextProvider, } from './ReactFiberContext.old'; -import { createFiberRoot } from './ReactFiberRoot.old'; -import { injectInternals, onScheduleRoot } from './ReactFiberDevToolsHook.old'; +import {createFiberRoot} from './ReactFiberRoot.old'; +import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.old'; import { requestEventTime, requestUpdateLane, @@ -89,7 +89,7 @@ import { } from './ReactFiberHotReloading.old'; import {markRenderScheduled} from './SchedulingProfiler'; import ReactVersion from 'shared/ReactVersion'; -export { registerMutableSourceForHydration } from './ReactMutableSource.old'; +export {registerMutableSourceForHydration} from './ReactMutableSource.old'; export {createPortal} from './ReactPortal'; export { createComponentSelector, From 53c0a6e34cdef0235ab27a0ca71fc7992116d128 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Wed, 29 Sep 2021 09:38:48 -0400 Subject: [PATCH 16/26] update test --- ...MServerSelectiveHydration-test.internal.js | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 111614e9cd422..910a5ca3153a2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -778,10 +778,8 @@ describe('ReactDOMServerSelectiveHydration', () => { suspend = true; - // A and D will be suspended. We'll click on D which should take - // priority, after we unsuspend. - const root = ReactDOM.createRoot(container, {hydrate: true}); - root.render(); + // A and D will be suspended. + const root = ReactDOM.hydrateRoot(container, ); // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); @@ -790,22 +788,18 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchMouseHoverEvent(spanB, spanD); dispatchMouseHoverEvent(spanC, spanB); + // C renders before B since its the current hover target + // B renders because its priority was increased when it was hovered over + expect(Scheduler).toHaveYielded(['App', 'C', 'B', 'Hover C']); + await act(async () => { suspend = false; resolve(); await promise; }); - // C renders before B since its the current hover target - // B renders because its priority was increased when it was hovered over - expect(Scheduler).toHaveYielded(['App', 'C', 'B', 'Hover C', 'D', 'A']); - - // We should prioritize hydrating D first because we clicked it. - // Next we should hydrate C since that's the current hover target. - // Next it doesn't matter if we hydrate A or B first but as an - // implementation detail we're currently hydrating B first since - // we at one point hovered over it and we never deprioritized it. - expect(Scheduler).toFlushAndYield([]); + // Finally D and A render + expect(Scheduler).toHaveYielded(['D', 'A']); document.body.removeChild(container); }); From ebf975cec74159df5afab180aebe28225a2330be Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Wed, 29 Sep 2021 09:54:05 -0400 Subject: [PATCH 17/26] remove unused var --- .../__tests__/ReactDOMServerSelectiveHydration-test.internal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 910a5ca3153a2..2f917c85f468c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -779,7 +779,7 @@ describe('ReactDOMServerSelectiveHydration', () => { suspend = true; // A and D will be suspended. - const root = ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, ); // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); From c86a63aed3c4fd6f29d424370396402d2d11a801 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Fri, 1 Oct 2021 11:28:08 -0400 Subject: [PATCH 18/26] gate changes --- ...DOMServerPartialHydration-test.internal.js | 159 ++++++++------ ...MServerSelectiveHydration-test.internal.js | 196 +++++++++++++----- packages/react-dom/src/client/ReactDOM.js | 3 + .../src/events/ReactDOMEventListener.js | 82 ++++++-- .../src/events/ReactDOMEventReplaying.js | 159 +++++++++++++- .../DOMPluginEventSystem-test.internal.js | 7 +- .../src/ReactFiberReconciler.js | 5 + .../src/ReactFiberReconciler.new.js | 14 ++ .../src/ReactFiberReconciler.old.js | 14 ++ packages/shared/ReactFeatureFlags.js | 2 + .../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 | 1 + .../shared/forks/ReactFeatureFlags.testing.js | 1 + .../forks/ReactFeatureFlags.testing.www.js | 1 + .../forks/ReactFeatureFlags.www-dynamic.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + 19 files changed, 502 insertions(+), 148 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 5a34c2ca89ebf..ec54c21f73c1f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1831,6 +1831,7 @@ describe('ReactDOMServerPartialHydration', () => { expect(newSpan.className).toBe('hi'); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a hydrated node until it commits', async () => { let suspend = false; let resolve; @@ -1905,21 +1906,15 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - // Clicks aren't replayed - expect(clicks).toBe(0); - expect(container.textContent).toBe('Click meHello'); - - await act(async () => { - a.click(); - }); - // Now click went through expect(clicks).toBe(1); + expect(container.textContent).toBe('Hello'); document.body.removeChild(container); }); // @gate www + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a hydrated event handle until it commits', async () => { const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; @@ -1998,16 +1993,12 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - // Clicks are not replayed - expect(onEvent).toHaveBeenCalledTimes(0); + expect(onEvent).toHaveBeenCalledTimes(2); - await act(async () => { - a.click(); - }); - expect(onEvent).toHaveBeenCalledTimes(1); document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('invokes discrete events on nested suspense boundaries in a root (legacy system)', async () => { let suspend = false; let resolve; @@ -2084,18 +2075,13 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - // clicks are not replayed - expect(clicks).toBe(0); - - await act(async () => { - a.click(); - }); - expect(clicks).toBe(1); + expect(clicks).toBe(2); document.body.removeChild(container); }); // @gate www + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('invokes discrete events on nested suspense boundaries in a root (createEventHandle)', async () => { let suspend = false; let isServerRendering = true; @@ -2176,20 +2162,12 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - - // Stays 0 because we don't replay clicks - expect(onEvent).toHaveBeenCalledTimes(0); - - await act(async () => { - a.click(); - }); - - // Clicks now go through after resolving - expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledTimes(2); document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke the parent of dehydrated boundary event', async () => { let suspend = false; let resolve; @@ -2258,13 +2236,6 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - // Stays 0 because we don't replay click events - expect(clicksOnChild).toBe(0); - - await act(async () => { - span.click(); - }); - // Now click events go through expect(clicksOnChild).toBe(1); // This will be zero due to the stopPropagation. expect(clicksOnParent).toBe(0); @@ -2272,6 +2243,7 @@ describe('ReactDOMServerPartialHydration', () => { document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => { let suspend = false; let resolve; @@ -2343,17 +2315,14 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - // We're now full hydrated but we don't replay clicks - expect(clicks).toBe(0); + // We're now full hydrated. - await act(async () => { - a.click(); - }); expect(clicks).toBe(1); document.body.removeChild(parentContainer); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('blocks only on the last continuous event (legacy system)', async () => { let suspend1 = false; let resolve1; @@ -2389,7 +2358,7 @@ describe('ReactDOMServerPartialHydration', () => { onMouseLeave={() => ops.push('Mouse Leave First')} /> {/* We suspend after to test what happens when we eager - attach the listener. */} + attach the listener. */} @@ -2434,11 +2403,9 @@ describe('ReactDOMServerPartialHydration', () => { expect(ops).toEqual([]); // Resolving the second promise so that rendering can complete. - await act(async () => { - suspend2 = false; - resolve2(); - await promise2; - }); + suspend2 = false; + resolve2(); + await promise2; Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -2447,12 +2414,10 @@ describe('ReactDOMServerPartialHydration', () => { // able to replay it now. expect(ops).toEqual(['Mouse Enter Second']); - await act(async () => { - // Resolving the first promise has no effect now. - suspend1 = false; - resolve1(); - await promise1; - }); + // Resolving the first promise has no effect now. + suspend1 = false; + resolve1(); + await promise1; Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -2543,6 +2508,7 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).not.toBe(null); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('regression test: does not overfire non-bubbling browser events', async () => { let suspend = false; let resolve; @@ -2622,18 +2588,6 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - // Submit event is not replayed - expect(submits).toBe(0); - expect(container.textContent).toBe('Click meHello'); - - await act(async () => { - form.dispatchEvent( - new Event('submit', { - bubbles: true, - }), - ); - }); - // Submit event has now gone through expect(submits).toBe(1); expect(container.textContent).toBe('Hello'); document.body.removeChild(container); @@ -2725,4 +2679,75 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).toBe(span); expect(ref.current.innerHTML).toBe('Hidden child'); }); + + // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + it('Does not replay discrete events', async () => { + let suspend = false; + let resolve; + const promise = new Promise(resolvePromise => (resolve = resolvePromise)); + + let clicks = 0; + + function Button() { + if (suspend) { + throw promise; + } + return ( + { + clicks++; + }}> + Click me + + ); + } + + function App() { + return ( +
+ +
+ ); + } + + const finalHTML = ReactDOMServer.renderToString(); + const container = document.createElement('div'); + container.innerHTML = finalHTML; + document.body.appendChild(container); + + const a = container.getElementsByTagName('a')[0]; + + // On the client we don't have all data yet but we want to start + // hydrating anyway. + suspend = true; + const root = ReactDOM.hydrateRoot(container, ); + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + + expect(container.textContent).toBe('Click me'); + + // We're now partially hydrated. + await act(async () => { + a.click(); + }); + expect(clicks).toBe(0); + + // Resolving the promise so that rendering can complete. + await act(async () => { + suspend = false; + resolve(); + await promise; + jest.runAllTimers(); + Scheduler.unstable_flushAll(); + }); + + // Event was not replayed + expect(clicks).toBe(0); + + expect(container.textContent).toBe('Click me'); + + document.body.removeChild(container); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 2f917c85f468c..2e5e55a07bd26 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -129,10 +129,8 @@ describe('ReactDOMServerSelectiveHydration', () => { Scheduler.unstable_yieldValue(text); return ( { - Scheduler.unstable_yieldValue('Capture Clicked ' + text); - }} onClick={e => { + e.preventDefault(); Scheduler.unstable_yieldValue('Clicked ' + text); }}> {text} @@ -174,15 +172,13 @@ describe('ReactDOMServerSelectiveHydration', () => { // This should synchronously hydrate the root App and the second suspense // boundary. - dispatchClickEvent(span); + const result = dispatchClickEvent(span); + + // The event should have been canceled because we called preventDefault. + expect(result).toBe(false); // We rendered App, B and then invoked the event without rendering A. - expect(Scheduler).toHaveYielded([ - 'App', - 'B', - 'Capture Clicked B', - 'Clicked B', - ]); + expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']); // After continuing the scheduler, we finally hydrate A. expect(Scheduler).toFlushAndYield(['A']); @@ -190,6 +186,7 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri if sync did not work first time', async () => { let suspend = false; let resolve; @@ -260,7 +257,7 @@ describe('ReactDOMServerSelectiveHydration', () => { }); expect(Scheduler).toHaveYielded([ 'App', - // B and C don't suspense so they are rendered immediately + // Continuing rendering will render B next. 'B', 'C', ]); @@ -270,13 +267,14 @@ describe('ReactDOMServerSelectiveHydration', () => { resolve(); await promise; }); - // After the click, we should prioritize hydrating D + // After the click, we should prioritize D and the Click first, // and only after that render A and C. - expect(Scheduler).toHaveYielded(['D', 'A']); + expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri for secondary discrete events', async () => { let suspend = false; let resolve; @@ -342,13 +340,12 @@ describe('ReactDOMServerSelectiveHydration', () => { // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); - // A and D cannot be hydrated yet because they Suspended. + // This click target cannot be hydrated yet because the first is Suspended. dispatchClickEvent(spanA); + dispatchClickEvent(spanC); dispatchClickEvent(spanD); - // C can be immediately hydrated in capture phase in time for it to be clicked - dispatchClickEvent(spanC); - expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']); + expect(Scheduler).toHaveYielded(['App']); await act(async () => { suspend = false; @@ -356,11 +353,15 @@ describe('ReactDOMServerSelectiveHydration', () => { await promise; }); - // We should prioritize hydrating A then D first since we clicked in + // We should prioritize hydrating A, C and D first since we clicked in // them. Only after they're done will we hydrate B. expect(Scheduler).toHaveYielded([ 'A', + 'Clicked A', + 'C', + 'Clicked C', 'D', + 'Clicked D', // B should render last since it wasn't clicked. 'B', ]); @@ -438,6 +439,7 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate www + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri if sync did not work first time (createEventHandle)', async () => { let suspend = false; let isServerRendering = true; @@ -513,20 +515,20 @@ describe('ReactDOMServerSelectiveHydration', () => { }); expect(Scheduler).toHaveYielded(['App', 'B', 'C']); - // After the click, we should prioritize D, - // and only after that render A + // After the click, we should prioritize D and the Click first, + // and only after that render A and C. await act(async () => { suspend = false; resolve(); await promise; }); - // No click is yielded since we don't replay clicks - expect(Scheduler).toHaveYielded(['D', 'A']); + expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); document.body.removeChild(container); }); // @gate www + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri for secondary discrete events (createEventHandle)', async () => { const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; @@ -598,12 +600,10 @@ describe('ReactDOMServerSelectiveHydration', () => { // This click target cannot be hydrated yet because the first is Suspended. createEventTarget(spanA).virtualclick(); - createEventTarget(spanD).virtualclick(); - - // C can be immediately hydrated in capture phase in time for click createEventTarget(spanC).virtualclick(); + createEventTarget(spanD).virtualclick(); - expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']); + expect(Scheduler).toHaveYielded(['App']); await act(async () => { suspend = false; @@ -615,7 +615,11 @@ describe('ReactDOMServerSelectiveHydration', () => { // them. Only after they're done will we hydrate B. expect(Scheduler).toHaveYielded([ 'A', + 'Clicked A', + 'C', + 'Clicked C', 'D', + 'Clicked D', // B should render last since it wasn't clicked. 'B', ]); @@ -623,6 +627,7 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates the hovered targets as higher priority for continuous events', async () => { let suspend = false; let resolve; @@ -699,8 +704,7 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchMouseHoverEvent(spanB, spanD); dispatchMouseHoverEvent(spanC, spanB); - // We can hydrate B and C now. - expect(Scheduler).toHaveYielded(['App', 'B', 'C']); + expect(Scheduler).toHaveYielded(['App']); await act(async () => { suspend = false; @@ -714,11 +718,19 @@ describe('ReactDOMServerSelectiveHydration', () => { // the same time since B was already scheduled. // This is ok because it will at least not continue for nested // boundary. See the next test below. - expect(Scheduler).toHaveYielded(['D', 'A']); + expect(Scheduler).toHaveYielded([ + 'D', + 'Clicked D', + 'B', // Ideally this should be later. + 'C', + 'Hover C', + 'A', + ]); document.body.removeChild(container); }); + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates the last target path first for continuous events', async () => { let suspend = false; let resolve; @@ -778,8 +790,10 @@ describe('ReactDOMServerSelectiveHydration', () => { suspend = true; - // A and D will be suspended. - ReactDOM.hydrateRoot(container, ); + // A and D will be suspended. We'll click on D which should take + // priority, after we unsuspend. + const root = ReactDOM.createRoot(container, {hydrate: true}); + root.render(); // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); @@ -788,18 +802,16 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchMouseHoverEvent(spanB, spanD); dispatchMouseHoverEvent(spanC, spanB); - // C renders before B since its the current hover target - // B renders because its priority was increased when it was hovered over - expect(Scheduler).toHaveYielded(['App', 'C', 'B', 'Hover C']); + suspend = false; + resolve(); + await promise; - await act(async () => { - suspend = false; - resolve(); - await promise; - }); - - // Finally D and A render - expect(Scheduler).toHaveYielded(['D', 'A']); + // We should prioritize hydrating D first because we clicked it. + // Next we should hydrate C since that's the current hover target. + // Next it doesn't matter if we hydrate A or B first but as an + // implementation detail we're currently hydrating B first since + // we at one point hovered over it and we never deprioritized it. + expect(Scheduler).toFlushAndYield(['App', 'C', 'Hover C', 'A', 'B', 'D']); document.body.removeChild(container); }); @@ -854,6 +866,7 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate experimental || www + // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates before an update even if hydration moves away from it', async () => { function Child({text}) { Scheduler.unstable_yieldValue(text); @@ -936,17 +949,20 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchClickEvent(spanC); expect(Scheduler).toHaveYielded([ - // Hydrate A and B since we hovered - // then Hydrate C since we clicked it. - 'A', - 'a', - 'B', - 'b', + // Hydrate C first since we clicked it. 'C', 'c', ]); expect(Scheduler).toFlushAndYield([ + // Finish hydration of A since we forced it to hydrate. + 'A', + 'a', + // Also, hydrate B since we hovered over it. + // It's not important which one comes first. A or B. + // As long as they both happen before the Idle update. + 'B', + 'b', // Begin the Idle update again. 'App', 'AA', @@ -961,4 +977,88 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); + + // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + it('Fires capture event handlers and native events if content is hydratable during discrete event', async () => { + function Child({text}) { + Scheduler.unstable_yieldValue(text); + const ref = React.useRef(); + React.useLayoutEffect(() => { + ref.current && + ref.current.addEventListener('click', () => { + Scheduler.unstable_yieldValue('Native Click ' + text); + }); + }, [text]); + return ( + { + Scheduler.unstable_yieldValue('Capture Clicked ' + text); + }} + onClick={e => { + Scheduler.unstable_yieldValue('Clicked ' + text); + }}> + {text} + + ); + } + + function App() { + Scheduler.unstable_yieldValue('App'); + return ( +
+ + + + + + +
+ ); + } + + spyOnDev(console, 'error'); + const finalHTML = ReactDOMServer.renderToString(); + expect(console.error).toHaveBeenCalledTimes(2); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'useLayoutEffect does nothing on the server', + ); + expect(console.error.calls.argsFor(1)[0]).toContain( + 'useLayoutEffect does nothing on the server', + ); + + expect(Scheduler).toHaveYielded(['App', 'A', 'B']); + + const container = document.createElement('div'); + // We need this to be in the document since we'll dispatch events on it. + document.body.appendChild(container); + + container.innerHTML = finalHTML; + + const span = container.getElementsByTagName('span')[1]; + + const root = ReactDOM.hydrateRoot(container, ); + + // Nothing has been hydrated so far. + expect(Scheduler).toHaveYielded([]); + + // This should synchronously hydrate the root App and the second suspense + // boundary. + dispatchClickEvent(span); + + // We rendered App, B and then invoked the event without rendering A. + expect(Scheduler).toHaveYielded([ + 'App', + 'B', + 'Capture Clicked B', + 'Native Click B', + 'Clicked B', + ]); + + // After continuing the scheduler, we finally hydrate A. + expect(Scheduler).toFlushAndYield(['A']); + + document.body.removeChild(container); + expect(console.error).toHaveBeenCalledTimes(2); + }); }); diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index e376e9f85010a..8e37995cb7364 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -28,6 +28,7 @@ import { flushControlled, injectIntoDevTools, attemptSynchronousHydration, + attemptDiscreteHydration, attemptContinuousHydration, attemptHydrationAtCurrentPriority, } from 'react-reconciler/src/ReactFiberReconciler'; @@ -52,6 +53,7 @@ import { import {restoreControlledState} from './ReactDOMComponent'; import { setAttemptSynchronousHydration, + setAttemptDiscreteHydration, setAttemptContinuousHydration, setAttemptHydrationAtCurrentPriority, queueExplicitHydrationTarget, @@ -66,6 +68,7 @@ import { } from '../events/ReactDOMControlledComponent'; setAttemptSynchronousHydration(attemptSynchronousHydration); +setAttemptDiscreteHydration(attemptDiscreteHydration); setAttemptContinuousHydration(attemptContinuousHydration); setAttemptHydrationAtCurrentPriority(attemptHydrationAtCurrentPriority); setGetCurrentUpdatePriority(getCurrentUpdatePriority); diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 8850be4f7bed6..2d4a4e6885c8a 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -11,13 +11,18 @@ import type {AnyNativeEvent} from '../events/PluginModuleType'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig'; import type {DOMEventName} from '../events/DOMEventNames'; - import { - attemptSynchronousEventHydration, + enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + enableSelectiveHydration, +} from 'shared/ReactFeatureFlags'; +import { + isReplayableDiscreteEvent, + queueDiscreteEvent, + hasQueuedDiscreteEvents, clearIfContinuousEvent, queueIfContinuousEvent, + attemptSynchronousHydration, } from './ReactDOMEventReplaying'; -import {enableSelectiveHydration} from 'shared/ReactFeatureFlags'; import { getNearestMountedFiber, getContainerFromFiber, @@ -160,6 +165,24 @@ export function dispatchEvent( // to filter them out until we fix the logic to handle them correctly. const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0; + if ( + allowReplay && + hasQueuedDiscreteEvents() && + isReplayableDiscreteEvent(domEventName) + ) { + // If we already have a queue of discrete events, and this is another discrete + // event, then we can't dispatch it regardless of its target, since they + // need to dispatch in order. + queueDiscreteEvent( + null, // Flags that we're not actually blocked on anything as far as we know. + domEventName, + eventSystemFlags, + targetContainer, + nativeEvent, + ); + return; + } + let blockedOn = attemptToDispatchEvent( domEventName, eventSystemFlags, @@ -168,32 +191,52 @@ export function dispatchEvent( ); if (blockedOn === null) { + // We successfully dispatched this event. if (allowReplay) { - // We successfully dispatched this event. clearIfContinuousEvent(domEventName, nativeEvent); } return; } - if ( - allowReplay && - queueIfContinuousEvent( - blockedOn, - domEventName, - eventSystemFlags, - targetContainer, - nativeEvent, - ) - ) { - return; + if (allowReplay) { + if ( + !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay && + isReplayableDiscreteEvent(domEventName) + ) { + // This this to be replayed later once the target is available. + queueDiscreteEvent( + blockedOn, + domEventName, + eventSystemFlags, + targetContainer, + nativeEvent, + ); + return; + } + if ( + queueIfContinuousEvent( + blockedOn, + domEventName, + eventSystemFlags, + targetContainer, + nativeEvent, + ) + ) { + return; + } + // We need to clear only if we didn't queue because + // queueing is accumulative. + clearIfContinuousEvent(domEventName, nativeEvent); } - // Synchronously hydrate non-replayable / non-continuous events - if (enableSelectiveHydration) { + if ( + enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay && + enableSelectiveHydration + ) { while (blockedOn !== null) { const fiber = getInstanceFromNode(blockedOn); if (fiber !== null) { - attemptSynchronousEventHydration(fiber); + attemptSynchronousHydration(fiber); } const nextBlockedOn = attemptToDispatchEvent( domEventName, @@ -207,9 +250,6 @@ export function dispatchEvent( blockedOn = nextBlockedOn; } } - // We need to clear only if we didn't queue because - // queueing is accumulative. - clearIfContinuousEvent(domEventName, nativeEvent); // This is not replayable so we'll invoke it but without a target, // in case the event system needs to trace it. diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index 6a29f3fa777d1..76f5958351f2e 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -14,7 +14,10 @@ import type {EventSystemFlags} from './EventSystemFlags'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities'; -import {enableSelectiveHydration} from 'shared/ReactFeatureFlags'; +import { + enableSelectiveHydration, + enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, +} from 'shared/ReactFeatureFlags'; import { unstable_scheduleCallback as scheduleCallback, unstable_NormalPriority as NormalPriority, @@ -32,14 +35,20 @@ import { import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags'; import {isHigherEventPriority} from 'react-reconciler/src/ReactEventPriorities'; -let attemptSynchronousHydration: (fiber: Object) => void; +let _attemptSynchronousHydration: (fiber: Object) => void; export function setAttemptSynchronousHydration(fn: (fiber: Object) => void) { - attemptSynchronousHydration = fn; + _attemptSynchronousHydration = fn; } -export function attemptSynchronousEventHydration(fiber: Object): void { - attemptSynchronousHydration(fiber); +export function attemptSynchronousHydration(fiber: Object) { + _attemptSynchronousHydration(fiber); +} + +let attemptDiscreteHydration: (fiber: Object) => void; + +export function setAttemptDiscreteHydration(fn: (fiber: Object) => void) { + attemptDiscreteHydration = fn; } let attemptContinuousHydration: (fiber: Object) => void; @@ -90,6 +99,9 @@ type QueuedReplayableEvent = {| let hasScheduledReplayAttempt = false; +// The queue of discrete events to be replayed. +const queuedDiscreteEvents: Array = []; + // Indicates if any continuous event targets are non-null for early bailout. const hasAnyQueuedContinuousEvents: boolean = false; // The last of each continuous event type. We only need to replay the last one @@ -109,10 +121,49 @@ type QueuedHydrationTarget = {| |}; const queuedExplicitHydrationTargets: Array = []; +export function hasQueuedDiscreteEvents(): boolean { + return queuedDiscreteEvents.length > 0; +} + export function hasQueuedContinuousEvents(): boolean { return hasAnyQueuedContinuousEvents; } +const discreteReplayableEvents: Array = [ + 'mousedown', + 'mouseup', + 'touchcancel', + 'touchend', + 'touchstart', + 'auxclick', + 'dblclick', + 'pointercancel', + 'pointerdown', + 'pointerup', + 'dragend', + 'dragstart', + 'drop', + 'compositionend', + 'compositionstart', + 'keydown', + 'keypress', + 'keyup', + 'input', + 'textInput', // Intentionally camelCase + 'copy', + 'cut', + 'paste', + 'click', + 'change', + 'contextmenu', + 'reset', + 'submit', +]; + +export function isReplayableDiscreteEvent(eventType: DOMEventName): boolean { + return discreteReplayableEvents.indexOf(eventType) > -1; +} + function createQueuedReplayableEvent( blockedOn: null | Container | SuspenseInstance, domEventName: DOMEventName, @@ -129,6 +180,50 @@ function createQueuedReplayableEvent( }; } +export function queueDiscreteEvent( + blockedOn: null | Container | SuspenseInstance, + domEventName: DOMEventName, + eventSystemFlags: EventSystemFlags, + targetContainer: EventTarget, + nativeEvent: AnyNativeEvent, +): void { + if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) { + return; + } + const queuedEvent = createQueuedReplayableEvent( + blockedOn, + domEventName, + eventSystemFlags, + targetContainer, + nativeEvent, + ); + queuedDiscreteEvents.push(queuedEvent); + if (enableSelectiveHydration) { + if (queuedDiscreteEvents.length === 1) { + // If this was the first discrete event, we might be able to + // synchronously unblock it so that preventDefault still works. + while (queuedEvent.blockedOn !== null) { + const fiber = getInstanceFromNode(queuedEvent.blockedOn); + if (fiber === null) { + break; + } + attemptSynchronousHydration(fiber); + if (queuedEvent.blockedOn === null) { + // We got unblocked by hydration. Let's try again. + replayUnblockedEvents(); + // If we're reblocked, on an inner boundary, we might need + // to attempt hydrating that one. + continue; + } else { + // We're still blocked from hydration, we have to give up + // and replay later. + break; + } + } + } + } +} + // Resets the replaying for this type of continuous event to no event. export function clearIfContinuousEvent( domEventName: DOMEventName, @@ -398,7 +493,44 @@ function attemptReplayContinuousQueuedEventInMap( function replayUnblockedEvents() { hasScheduledReplayAttempt = false; - // Replay continuous events. + if (!enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) { + // First replay discrete events. + while (queuedDiscreteEvents.length > 0) { + const nextDiscreteEvent = queuedDiscreteEvents[0]; + if (nextDiscreteEvent.blockedOn !== null) { + // We're still blocked. + // Increase the priority of this boundary to unblock + // the next discrete event. + const fiber = getInstanceFromNode(nextDiscreteEvent.blockedOn); + if (fiber !== null) { + attemptDiscreteHydration(fiber); + } + break; + } + const targetContainers = nextDiscreteEvent.targetContainers; + while (targetContainers.length > 0) { + const targetContainer = targetContainers[0]; + const nextBlockedOn = attemptToDispatchEvent( + nextDiscreteEvent.domEventName, + nextDiscreteEvent.eventSystemFlags, + targetContainer, + nextDiscreteEvent.nativeEvent, + ); + if (nextBlockedOn !== null) { + // We're still blocked. Try again later. + nextDiscreteEvent.blockedOn = nextBlockedOn; + break; + } + // This target container was successfully dispatched. Try the next. + targetContainers.shift(); + } + if (nextDiscreteEvent.blockedOn === null) { + // We've successfully replayed the first event. Let's try the next one. + queuedDiscreteEvents.shift(); + } + } + } + // Next replay any continuous events. if (queuedFocus !== null && attemptReplayContinuousQueuedEvent(queuedFocus)) { queuedFocus = null; } @@ -431,6 +563,21 @@ function scheduleCallbackIfUnblocked( export function retryIfBlockedOn( unblocked: Container | SuspenseInstance, ): void { + // Mark anything that was blocked on this as no longer blocked + // and eligible for a replay. + if (queuedDiscreteEvents.length > 0) { + scheduleCallbackIfUnblocked(queuedDiscreteEvents[0], unblocked); + // This is a exponential search for each boundary that commits. I think it's + // worth it because we expect very few discrete events to queue up and once + // we are actually fully unblocked it will be fast to replay them. + for (let i = 1; i < queuedDiscreteEvents.length; i++) { + const queuedEvent = queuedDiscreteEvents[i]; + if (queuedEvent.blockedOn === unblocked) { + queuedEvent.blockedOn = null; + } + } + } + if (queuedFocus !== null) { scheduleCallbackIfUnblocked(queuedFocus, unblocked); } diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js index 1798a464ac288..4f2df6fffc495 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -647,13 +647,8 @@ describe('DOMPluginEventSystem', () => { await promise; }); - // We're now fully hydrated. But the click isn't replayed. - expect(clicks).toBe(0); + // We're now full hydrated. - await act(async () => { - a.click(); - }); - // Clicks can go through now expect(clicks).toBe(1); document.body.removeChild(parentContainer); diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 7dac786438236..6e992ddd7d3b1 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -26,6 +26,7 @@ import { flushPassiveEffects as flushPassiveEffects_old, getPublicRootInstance as getPublicRootInstance_old, attemptSynchronousHydration as attemptSynchronousHydration_old, + attemptDiscreteHydration as attemptDiscreteHydration_old, attemptContinuousHydration as attemptContinuousHydration_old, attemptHydrationAtCurrentPriority as attemptHydrationAtCurrentPriority_old, findHostInstance as findHostInstance_old, @@ -62,6 +63,7 @@ import { flushPassiveEffects as flushPassiveEffects_new, getPublicRootInstance as getPublicRootInstance_new, attemptSynchronousHydration as attemptSynchronousHydration_new, + attemptDiscreteHydration as attemptDiscreteHydration_new, attemptContinuousHydration as attemptContinuousHydration_new, attemptHydrationAtCurrentPriority as attemptHydrationAtCurrentPriority_new, findHostInstance as findHostInstance_new, @@ -117,6 +119,9 @@ export const getPublicRootInstance = enableNewReconciler export const attemptSynchronousHydration = enableNewReconciler ? attemptSynchronousHydration_new : attemptSynchronousHydration_old; +export const attemptDiscreteHydration = enableNewReconciler + ? attemptDiscreteHydration_new + : attemptDiscreteHydration_old; export const attemptContinuousHydration = enableNewReconciler ? attemptContinuousHydration_new : attemptContinuousHydration_old; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index e22b4a1c557c9..6c42165a30347 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -387,6 +387,20 @@ function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) { } } +export function attemptDiscreteHydration(fiber: Fiber): void { + if (fiber.tag !== SuspenseComponent) { + // We ignore HostRoots here because we can't increase + // their priority and they should not suspend on I/O, + // since you have to wrap anything that might suspend in + // Suspense. + return; + } + const eventTime = requestEventTime(); + const lane = SyncLane; + scheduleUpdateOnFiber(fiber, lane, eventTime); + markRetryLaneIfNotHydrated(fiber, lane); +} + export function attemptContinuousHydration(fiber: Fiber): void { if (fiber.tag !== SuspenseComponent) { // We ignore HostRoots here because we can't increase diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index badd11359ec0b..7693cc7fe4006 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -387,6 +387,20 @@ function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) { } } +export function attemptDiscreteHydration(fiber: Fiber): void { + if (fiber.tag !== SuspenseComponent) { + // We ignore HostRoots here because we can't increase + // their priority and they should not suspend on I/O, + // since you have to wrap anything that might suspend in + // Suspense. + return; + } + const eventTime = requestEventTime(); + const lane = SyncLane; + scheduleUpdateOnFiber(fiber, lane, eventTime); + markRetryLaneIfNotHydrated(fiber, lane); +} + export function attemptContinuousHydration(fiber: Fiber): void { if (fiber.tag !== SuspenseComponent) { // We ignore HostRoots here because we can't increase diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 608db694be518..f5d34e2ff6539 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -103,6 +103,8 @@ export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; + export const enableComponentStackLocations = true; export const enableNewReconciler = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 9c3d45528c5af..5a1c1ed8d9c60 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -50,6 +50,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = false; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index bf96cc00e687c..3c11070d6ecb3 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = false; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 0d2b226adf2f6..4b0457c219587 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index f760425ce03cb..2c54c1fb77c20 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -51,6 +51,7 @@ export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableStrictEffects = false; export const createRootStrictEffectsByDefault = false; export const enableUseRefAccessWarning = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index cb0efcc34bdcd..be60bcfbddb77 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index d1cfdbb749565..76af047ab6d4e 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 62a10f475acaa..6fc6cf3c68367 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const warnAboutSpreadingKeyToJSX = false; export const warnOnSubscriptionInsideStartTransition = false; export const enableSuspenseAvoidThisFallback = false; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = !__EXPERIMENTAL__; export const enableFilterEmptyStringAttributesDOM = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 6400ef0422503..ef980529f3f21 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -27,6 +27,7 @@ export const enableLazyContextPropagation = __VARIANT__; export const enableSyncDefaultUpdates = __VARIANT__; export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__; export const warnOnSubscriptionInsideStartTransition = __VARIANT__; +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__; // Enable this flag to help with concurrent mode debugging. // It logs information to the console about React scheduling, rendering, and commit phases. diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index c5dde53578c95..0f1ce6eb1f9f7 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -32,6 +32,7 @@ export const { enableLazyContextPropagation, enableSyncDefaultUpdates, warnOnSubscriptionInsideStartTransition, + enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build. From 8e49d1195cd6e07572c6dfedeedebfd1006c9d46 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Fri, 1 Oct 2021 11:34:14 -0400 Subject: [PATCH 19/26] lint --- .../ReactDOMServerPartialHydration-test.internal.js | 2 +- ...ReactDOMServerSelectiveHydration-test.internal.js | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index ec54c21f73c1f..d469d30e89204 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -2722,7 +2722,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - const root = ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, ); Scheduler.unstable_flushAll(); jest.runAllTimers(); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 2e5e55a07bd26..d49eef9b539e7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -984,10 +984,12 @@ describe('ReactDOMServerSelectiveHydration', () => { Scheduler.unstable_yieldValue(text); const ref = React.useRef(); React.useLayoutEffect(() => { - ref.current && - ref.current.addEventListener('click', () => { - Scheduler.unstable_yieldValue('Native Click ' + text); - }); + if (!ref.current) { + return; + } + ref.current.addEventListener('click', () => { + Scheduler.unstable_yieldValue('Native Click ' + text); + }); }, [text]); return ( { const span = container.getElementsByTagName('span')[1]; - const root = ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, ); // Nothing has been hydrated so far. expect(Scheduler).toHaveYielded([]); From b23468e26582889f9f144e776439f973843121c1 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Fri, 1 Oct 2021 11:49:58 -0400 Subject: [PATCH 20/26] fix test --- ...MServerSelectiveHydration-test.internal.js | 22 +++++++++++-------- .../DOMPluginEventSystem-test.internal.js | 8 ++++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index d49eef9b539e7..b6f48b311d039 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -980,6 +980,7 @@ describe('ReactDOMServerSelectiveHydration', () => { // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('Fires capture event handlers and native events if content is hydratable during discrete event', async () => { + spyOnDev(console, 'error'); function Child({text}) { Scheduler.unstable_yieldValue(text); const ref = React.useRef(); @@ -1019,15 +1020,16 @@ describe('ReactDOMServerSelectiveHydration', () => { ); } - spyOnDev(console, 'error'); const finalHTML = ReactDOMServer.renderToString(); - expect(console.error).toHaveBeenCalledTimes(2); - expect(console.error.calls.argsFor(0)[0]).toContain( - 'useLayoutEffect does nothing on the server', - ); - expect(console.error.calls.argsFor(1)[0]).toContain( - 'useLayoutEffect does nothing on the server', - ); + if (__DEV__) { + expect(console.error).toHaveBeenCalledTimes(2); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'useLayoutEffect does nothing on the server', + ); + expect(console.error.calls.argsFor(1)[0]).toContain( + 'useLayoutEffect does nothing on the server', + ); + } expect(Scheduler).toHaveYielded(['App', 'A', 'B']); @@ -1061,6 +1063,8 @@ describe('ReactDOMServerSelectiveHydration', () => { expect(Scheduler).toFlushAndYield(['A']); document.body.removeChild(container); - expect(console.error).toHaveBeenCalledTimes(2); + if (__DEV__) { + expect(console.error).toHaveBeenCalledTimes(2); + } }); }); diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js index 4f2df6fffc495..a7ec31781603e 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -649,7 +649,13 @@ describe('DOMPluginEventSystem', () => { // We're now full hydrated. - expect(clicks).toBe(1); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(clicks).toBe(0); + } else { + expect(clicks).toBe(1); + } document.body.removeChild(parentContainer); }); From c042e6e2efd94d3219d81befd556caeb1672f344 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Fri, 1 Oct 2021 12:07:34 -0400 Subject: [PATCH 21/26] only gate small parts of some tests --- ...DOMServerPartialHydration-test.internal.js | 79 ++++++++++++++----- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index d469d30e89204..2368acc355ee8 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1831,7 +1831,6 @@ describe('ReactDOMServerPartialHydration', () => { expect(newSpan.className).toBe('hi'); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a hydrated node until it commits', async () => { let suspend = false; let resolve; @@ -1906,15 +1905,20 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(clicks).toBe(1); - - expect(container.textContent).toBe('Hello'); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(clicks).toBe(0); + expect(container.textContent).toBe('Click meHello'); + } else { + expect(clicks).toBe(1); + expect(container.textContent).toBe('Hello'); + } document.body.removeChild(container); }); // @gate www - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a hydrated event handle until it commits', async () => { const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; @@ -1993,12 +1997,17 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - expect(onEvent).toHaveBeenCalledTimes(2); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(onEvent).toHaveBeenCalledTimes(0); + } else { + expect(onEvent).toHaveBeenCalledTimes(2); + } document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('invokes discrete events on nested suspense boundaries in a root (legacy system)', async () => { let suspend = false; let resolve; @@ -2075,13 +2084,19 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(clicks).toBe(2); + + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(clicks).toBe(0); + } else { + expect(clicks).toBe(2); + } document.body.removeChild(container); }); // @gate www - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('invokes discrete events on nested suspense boundaries in a root (createEventHandle)', async () => { let suspend = false; let isServerRendering = true; @@ -2162,12 +2177,17 @@ describe('ReactDOMServerPartialHydration', () => { resolve(); await promise; }); - expect(onEvent).toHaveBeenCalledTimes(2); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(onEvent).toHaveBeenCalledTimes(0); + } else { + expect(onEvent).toHaveBeenCalledTimes(2); + } document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke the parent of dehydrated boundary event', async () => { let suspend = false; let resolve; @@ -2236,14 +2256,20 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - expect(clicksOnChild).toBe(1); - // This will be zero due to the stopPropagation. - expect(clicksOnParent).toBe(0); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(clicksOnChild).toBe(0); + expect(clicksOnParent).toBe(0); + } else { + expect(clicksOnChild).toBe(1); + // This will be zero due to the stopPropagation. + expect(clicksOnParent).toBe(0); + } document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => { let suspend = false; let resolve; @@ -2316,8 +2342,13 @@ describe('ReactDOMServerPartialHydration', () => { }); // We're now full hydrated. - - expect(clicks).toBe(1); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(clicks).toBe(0); + } else { + expect(clicks).toBe(1); + } document.body.removeChild(parentContainer); }); @@ -2508,7 +2539,6 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).not.toBe(null); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('regression test: does not overfire non-bubbling browser events', async () => { let suspend = false; let resolve; @@ -2588,8 +2618,17 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); - expect(submits).toBe(1); - expect(container.textContent).toBe('Hello'); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + // discrete event not replayed + expect(submits).toBe(0); + expect(container.textContent).toBe('Click meHello'); + } else { + expect(submits).toBe(1); + expect(container.textContent).toBe('Hello'); + } + document.body.removeChild(container); }); From ab9a113d58b5c632e7aa84d40a70417aa4b9d104 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Fri, 1 Oct 2021 16:01:06 -0400 Subject: [PATCH 22/26] use gate function in tests instead --- ...DOMServerPartialHydration-test.internal.js | 35 +++++++++++++++---- .../DOMPluginEventSystem-test.internal.js | 5 ++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 2368acc355ee8..5bc17c9bc0a30 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1907,7 +1907,10 @@ describe('ReactDOMServerPartialHydration', () => { }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(clicks).toBe(0); expect(container.textContent).toBe('Click meHello'); @@ -1998,7 +2001,10 @@ describe('ReactDOMServerPartialHydration', () => { }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(onEvent).toHaveBeenCalledTimes(0); } else { @@ -2086,7 +2092,10 @@ describe('ReactDOMServerPartialHydration', () => { }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(clicks).toBe(0); } else { @@ -2178,7 +2187,10 @@ describe('ReactDOMServerPartialHydration', () => { await promise; }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(onEvent).toHaveBeenCalledTimes(0); } else { @@ -2257,7 +2269,10 @@ describe('ReactDOMServerPartialHydration', () => { }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(clicksOnChild).toBe(0); expect(clicksOnParent).toBe(0); @@ -2343,7 +2358,10 @@ describe('ReactDOMServerPartialHydration', () => { // We're now full hydrated. if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(clicks).toBe(0); } else { @@ -2619,7 +2637,10 @@ describe('ReactDOMServerPartialHydration', () => { }); if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { // discrete event not replayed expect(submits).toBe(0); diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js index a7ec31781603e..1b2bfbb0bef4d 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -650,7 +650,10 @@ describe('DOMPluginEventSystem', () => { // We're now full hydrated. if ( - ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) ) { expect(clicks).toBe(0); } else { From e2a929be78b3955f6e1dee3e0f6d68cb8915c52c Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Mon, 4 Oct 2021 09:31:38 -0400 Subject: [PATCH 23/26] incorporate feedback and fix bug affecting continuous events --- ...DOMServerPartialHydration-test.internal.js | 72 ------- ...MServerSelectiveHydration-test.internal.js | 185 ++++++++++++------ .../src/events/ReactDOMEventListener.js | 4 +- .../src/events/ReactDOMEventReplaying.js | 7 + 4 files changed, 137 insertions(+), 131 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 5bc17c9bc0a30..b1a88868b3389 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -2371,7 +2371,6 @@ describe('ReactDOMServerPartialHydration', () => { document.body.removeChild(parentContainer); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('blocks only on the last continuous event (legacy system)', async () => { let suspend1 = false; let resolve1; @@ -2739,75 +2738,4 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).toBe(span); expect(ref.current.innerHTML).toBe('Hidden child'); }); - - // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay - it('Does not replay discrete events', async () => { - let suspend = false; - let resolve; - const promise = new Promise(resolvePromise => (resolve = resolvePromise)); - - let clicks = 0; - - function Button() { - if (suspend) { - throw promise; - } - return ( - { - clicks++; - }}> - Click me - - ); - } - - function App() { - return ( -
- -
- ); - } - - const finalHTML = ReactDOMServer.renderToString(); - const container = document.createElement('div'); - container.innerHTML = finalHTML; - document.body.appendChild(container); - - const a = container.getElementsByTagName('a')[0]; - - // On the client we don't have all data yet but we want to start - // hydrating anyway. - suspend = true; - ReactDOM.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - expect(container.textContent).toBe('Click me'); - - // We're now partially hydrated. - await act(async () => { - a.click(); - }); - expect(clicks).toBe(0); - - // Resolving the promise so that rendering can complete. - await act(async () => { - suspend = false; - resolve(); - await promise; - jest.runAllTimers(); - Scheduler.unstable_flushAll(); - }); - - // Event was not replayed - expect(clicks).toBe(0); - - expect(container.textContent).toBe('Click me'); - - document.body.removeChild(container); - }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index b6f48b311d039..ec4d4c76f5750 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -14,6 +14,7 @@ import {createEventTarget} from 'dom-event-testing-library'; let React; let ReactDOM; let ReactDOMServer; +let ReactFeatureFlags; let Scheduler; let Suspense; let act; @@ -112,7 +113,7 @@ describe('ReactDOMServerSelectiveHydration', () => { beforeEach(() => { jest.resetModuleRegistry(); - const ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableCreateEventHandleAPI = true; React = require('react'); ReactDOM = require('react-dom'); @@ -186,7 +187,6 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri if sync did not work first time', async () => { let suspend = false; let resolve; @@ -267,14 +267,23 @@ describe('ReactDOMServerSelectiveHydration', () => { resolve(); await promise; }); - // After the click, we should prioritize D and the Click first, - // and only after that render A and C. - expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); + + if ( + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) + ) { + expect(Scheduler).toHaveYielded(['D', 'A']); + } else { + // After the click, we should prioritize D and the Click first, + // and only after that render A and C. + expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); + } document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri for secondary discrete events', async () => { let suspend = false; let resolve; @@ -345,7 +354,16 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchClickEvent(spanC); dispatchClickEvent(spanD); - expect(Scheduler).toHaveYielded(['App']); + if ( + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) + ) { + expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']); + } else { + expect(Scheduler).toHaveYielded(['App']); + } await act(async () => { suspend = false; @@ -353,18 +371,29 @@ describe('ReactDOMServerSelectiveHydration', () => { await promise; }); - // We should prioritize hydrating A, C and D first since we clicked in - // them. Only after they're done will we hydrate B. - expect(Scheduler).toHaveYielded([ - 'A', - 'Clicked A', - 'C', - 'Clicked C', - 'D', - 'Clicked D', - // B should render last since it wasn't clicked. - 'B', - ]); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(Scheduler).toHaveYielded([ + 'A', + 'D', + // B should render last since it wasn't clicked. + 'B', + ]); + } else { + // We should prioritize hydrating A, C and D first since we clicked in + // them. Only after they're done will we hydrate B. + expect(Scheduler).toHaveYielded([ + 'A', + 'Clicked A', + 'C', + 'Clicked C', + 'D', + 'Clicked D', + // B should render last since it wasn't clicked. + 'B', + ]); + } document.body.removeChild(container); }); @@ -439,7 +468,6 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate www - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri if sync did not work first time (createEventHandle)', async () => { let suspend = false; let isServerRendering = true; @@ -522,13 +550,22 @@ describe('ReactDOMServerSelectiveHydration', () => { resolve(); await promise; }); - expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); + if ( + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) + ) { + // no replay + expect(Scheduler).toHaveYielded(['D', 'A']); + } else { + expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']); + } document.body.removeChild(container); }); // @gate www - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri for secondary discrete events (createEventHandle)', async () => { const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; @@ -603,31 +640,46 @@ describe('ReactDOMServerSelectiveHydration', () => { createEventTarget(spanC).virtualclick(); createEventTarget(spanD).virtualclick(); - expect(Scheduler).toHaveYielded(['App']); - + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']); + } else { + expect(Scheduler).toHaveYielded(['App']); + } await act(async () => { suspend = false; resolve(); await promise; }); - // We should prioritize hydrating A, C and D first since we clicked in - // them. Only after they're done will we hydrate B. - expect(Scheduler).toHaveYielded([ - 'A', - 'Clicked A', - 'C', - 'Clicked C', - 'D', - 'Clicked D', - // B should render last since it wasn't clicked. - 'B', - ]); + if ( + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay + ) { + expect(Scheduler).toHaveYielded([ + 'A', + 'D', + // B should render last since it wasn't clicked. + 'B', + ]); + } else { + // We should prioritize hydrating A, C and D first since we clicked in + // them. Only after they're done will we hydrate B. + expect(Scheduler).toHaveYielded([ + 'A', + 'Clicked A', + 'C', + 'Clicked C', + 'D', + 'Clicked D', + // B should render last since it wasn't clicked. + 'B', + ]); + } document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates the hovered targets as higher priority for continuous events', async () => { let suspend = false; let resolve; @@ -712,25 +764,41 @@ describe('ReactDOMServerSelectiveHydration', () => { await promise; }); - // We should prioritize hydrating D first because we clicked it. - // Next we should hydrate C since that's the current hover target. - // To simplify implementation details we hydrate both B and C at - // the same time since B was already scheduled. - // This is ok because it will at least not continue for nested - // boundary. See the next test below. - expect(Scheduler).toHaveYielded([ - 'D', - 'Clicked D', - 'B', // Ideally this should be later. - 'C', - 'Hover C', - 'A', - ]); + if ( + gate( + flags => + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay, + ) + ) { + // We should prioritize hydrating D first because we clicked it. + // but event isnt replayed + expect(Scheduler).toHaveYielded([ + 'D', + 'B', // Ideally this should be later. + 'C', + 'Hover C', + 'A', + ]); + } else { + // We should prioritize hydrating D first because we clicked it. + // Next we should hydrate C since that's the current hover target. + // To simplify implementation details we hydrate both B and C at + // the same time since B was already scheduled. + // This is ok because it will at least not continue for nested + // boundary. See the next test below. + expect(Scheduler).toHaveYielded([ + 'D', + 'Clicked D', + 'B', // Ideally this should be later. + 'C', + 'Hover C', + 'A', + ]); + } document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates the last target path first for continuous events', async () => { let suspend = false; let resolve; @@ -802,16 +870,18 @@ describe('ReactDOMServerSelectiveHydration', () => { dispatchMouseHoverEvent(spanB, spanD); dispatchMouseHoverEvent(spanC, spanB); - suspend = false; - resolve(); - await promise; + await act(async () => { + suspend = false; + resolve(); + await promise; + }); // We should prioritize hydrating D first because we clicked it. // Next we should hydrate C since that's the current hover target. // Next it doesn't matter if we hydrate A or B first but as an // implementation detail we're currently hydrating B first since // we at one point hovered over it and we never deprioritized it. - expect(Scheduler).toFlushAndYield(['App', 'C', 'Hover C', 'A', 'B', 'D']); + expect(Scheduler).toHaveYielded(['App', 'C', 'Hover C', 'A', 'B', 'D']); document.body.removeChild(container); }); @@ -866,7 +936,6 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate experimental || www - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates before an update even if hydration moves away from it', async () => { function Child({text}) { Scheduler.unstable_yieldValue(text); @@ -979,7 +1048,7 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay - it('Fires capture event handlers and native events if content is hydratable during discrete event', async () => { + it('fires capture event handlers and native events if content is hydratable during discrete event', async () => { spyOnDev(console, 'error'); function Child({text}) { Scheduler.unstable_yieldValue(text); diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 2d4a4e6885c8a..6467421d4f635 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -22,6 +22,7 @@ import { clearIfContinuousEvent, queueIfContinuousEvent, attemptSynchronousHydration, + isCapturePhaseSynchronouslyHydratableEvent, } from './ReactDOMEventReplaying'; import { getNearestMountedFiber, @@ -231,7 +232,8 @@ export function dispatchEvent( if ( enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay && - enableSelectiveHydration + enableSelectiveHydration && + isCapturePhaseSynchronouslyHydratableEvent(domEventName) ) { while (blockedOn !== null) { const fiber = getInstanceFromNode(blockedOn); diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index 76f5958351f2e..c363721bcd848 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -300,6 +300,13 @@ function accumulateOrCreateContinuousQueuedReplayableEvent( return existingQueuedEvent; } +export function isCapturePhaseSynchronouslyHydratableEvent( + eventName: DOMEventName, +) { + // TODO: maybe include more events + return isReplayableDiscreteEvent(eventName); +} + export function queueIfContinuousEvent( blockedOn: null | Container | SuspenseInstance, domEventName: DOMEventName, From 19a1520327ca6619e93aafa828764f51bc7fb9ca Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Mon, 4 Oct 2021 09:36:53 -0400 Subject: [PATCH 24/26] remove redudant test --- ...DOMServerPartialHydration-test.internal.js | 71 ------------------- 1 file changed, 71 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 5bc17c9bc0a30..e059689cffe1a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -2739,75 +2739,4 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).toBe(span); expect(ref.current.innerHTML).toBe('Hidden child'); }); - - // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay - it('Does not replay discrete events', async () => { - let suspend = false; - let resolve; - const promise = new Promise(resolvePromise => (resolve = resolvePromise)); - - let clicks = 0; - - function Button() { - if (suspend) { - throw promise; - } - return ( - { - clicks++; - }}> - Click me - - ); - } - - function App() { - return ( -
- -
- ); - } - - const finalHTML = ReactDOMServer.renderToString(); - const container = document.createElement('div'); - container.innerHTML = finalHTML; - document.body.appendChild(container); - - const a = container.getElementsByTagName('a')[0]; - - // On the client we don't have all data yet but we want to start - // hydrating anyway. - suspend = true; - ReactDOM.hydrateRoot(container, ); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - expect(container.textContent).toBe('Click me'); - - // We're now partially hydrated. - await act(async () => { - a.click(); - }); - expect(clicks).toBe(0); - - // Resolving the promise so that rendering can complete. - await act(async () => { - suspend = false; - resolve(); - await promise; - jest.runAllTimers(); - Scheduler.unstable_flushAll(); - }); - - // Event was not replayed - expect(clicks).toBe(0); - - expect(container.textContent).toBe('Click me'); - - document.body.removeChild(container); - }); }); From b1c9e625a0fa0be8fa0f304f377ee6e38cb6430e Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Mon, 4 Oct 2021 09:43:04 -0400 Subject: [PATCH 25/26] remote gating from bad merge --- .../ReactDOMServerPartialHydration-test.internal.js | 1 - .../ReactDOMServerSelectiveHydration-test.internal.js | 7 ------- 2 files changed, 8 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index e059689cffe1a..b1a88868b3389 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -2371,7 +2371,6 @@ describe('ReactDOMServerPartialHydration', () => { document.body.removeChild(parentContainer); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('blocks only on the last continuous event (legacy system)', async () => { let suspend1 = false; let resolve1; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 1c893b32c8173..ec4d4c76f5750 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -187,7 +187,6 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri if sync did not work first time', async () => { let suspend = false; let resolve; @@ -285,7 +284,6 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri for secondary discrete events', async () => { let suspend = false; let resolve; @@ -470,7 +468,6 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate www - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri if sync did not work first time (createEventHandle)', async () => { let suspend = false; let isServerRendering = true; @@ -569,7 +566,6 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate www - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates at higher pri for secondary discrete events (createEventHandle)', async () => { const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; @@ -684,7 +680,6 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates the hovered targets as higher priority for continuous events', async () => { let suspend = false; let resolve; @@ -804,7 +799,6 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates the last target path first for continuous events', async () => { let suspend = false; let resolve; @@ -942,7 +936,6 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate experimental || www - // @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay it('hydrates before an update even if hydration moves away from it', async () => { function Child({text}) { Scheduler.unstable_yieldValue(text); From c10215e6c31593034302f9a8014853023d4a696d Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Mon, 4 Oct 2021 09:46:21 -0400 Subject: [PATCH 26/26] and couple more changes wipe out from rebase/pull o.O --- ...MServerSelectiveHydration-test.internal.js | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index ec4d4c76f5750..bb04b46df1c40 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -1057,9 +1057,9 @@ describe('ReactDOMServerSelectiveHydration', () => { if (!ref.current) { return; } - ref.current.addEventListener('click', () => { + ref.current.onclick = () => { Scheduler.unstable_yieldValue('Native Click ' + text); - }); + }; }, [text]); return ( { ); } - const finalHTML = ReactDOMServer.renderToString(); - if (__DEV__) { - expect(console.error).toHaveBeenCalledTimes(2); - expect(console.error.calls.argsFor(0)[0]).toContain( - 'useLayoutEffect does nothing on the server', - ); - expect(console.error.calls.argsFor(1)[0]).toContain( - 'useLayoutEffect does nothing on the server', - ); - } + let finalHTML; + expect(() => { + finalHTML = ReactDOMServer.renderToString(); + }).toErrorDev([ + 'useLayoutEffect does nothing on the server', + 'useLayoutEffect does nothing on the server', + ]); expect(Scheduler).toHaveYielded(['App', 'A', 'B']); @@ -1132,8 +1129,5 @@ describe('ReactDOMServerSelectiveHydration', () => { expect(Scheduler).toFlushAndYield(['A']); document.body.removeChild(container); - if (__DEV__) { - expect(console.error).toHaveBeenCalledTimes(2); - } }); });