Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync hydrate discrete events in capture phase and dont replay discrete events #22448

Merged
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1905,10 +1905,19 @@ describe('ReactDOMServerPartialHydration', () => {
resolve();
await promise;
});
expect(clicks).toBe(1);

expect(container.textContent).toBe('Hello');

if (
gate(
flags =>
flags.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);
});

Expand Down Expand Up @@ -1991,7 +2000,16 @@ describe('ReactDOMServerPartialHydration', () => {
await promise;
});

expect(onEvent).toHaveBeenCalledTimes(2);
if (
gate(
flags =>
flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
)
) {
expect(onEvent).toHaveBeenCalledTimes(0);
} else {
expect(onEvent).toHaveBeenCalledTimes(2);
}

document.body.removeChild(container);
});
Expand Down Expand Up @@ -2072,7 +2090,17 @@ describe('ReactDOMServerPartialHydration', () => {
resolve();
await promise;
});
expect(clicks).toBe(2);

if (
gate(
flags =>
flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
)
) {
expect(clicks).toBe(0);
} else {
expect(clicks).toBe(2);
}

document.body.removeChild(container);
});
Expand Down Expand Up @@ -2158,7 +2186,16 @@ describe('ReactDOMServerPartialHydration', () => {
resolve();
await promise;
});
expect(onEvent).toHaveBeenCalledTimes(2);
if (
gate(
flags =>
flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
)
) {
expect(onEvent).toHaveBeenCalledTimes(0);
} else {
expect(onEvent).toHaveBeenCalledTimes(2);
}

document.body.removeChild(container);
});
Expand Down Expand Up @@ -2231,9 +2268,19 @@ describe('ReactDOMServerPartialHydration', () => {
await promise;
});

expect(clicksOnChild).toBe(1);
// This will be zero due to the stopPropagation.
expect(clicksOnParent).toBe(0);
if (
gate(
flags =>
flags.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);
});
Expand Down Expand Up @@ -2310,12 +2357,21 @@ describe('ReactDOMServerPartialHydration', () => {
});

// We're now full hydrated.

expect(clicks).toBe(1);
if (
gate(
flags =>
flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
)
) {
expect(clicks).toBe(0);
} else {
expect(clicks).toBe(1);
}

document.body.removeChild(parentContainer);
});

// @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
salazarm marked this conversation as resolved.
Show resolved Hide resolved
it('blocks only on the last continuous event (legacy system)', async () => {
let suspend1 = false;
let resolve1;
Expand Down Expand Up @@ -2580,8 +2636,20 @@ describe('ReactDOMServerPartialHydration', () => {
await promise;
});

expect(submits).toBe(1);
expect(container.textContent).toBe('Hello');
if (
gate(
flags =>
flags.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);
});

Expand Down Expand Up @@ -2671,4 +2739,75 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
expect(ref.current.innerHTML).toBe('Hidden child');
});

// @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
it('Does not replay discrete events', async () => {
salazarm marked this conversation as resolved.
Show resolved Hide resolved
let suspend = false;
let resolve;
const promise = new Promise(resolvePromise => (resolve = resolvePromise));

let clicks = 0;

function Button() {
if (suspend) {
throw promise;
}
return (
<a
onClick={() => {
clicks++;
}}>
Click me
</a>
);
}

function App() {
return (
<div>
<Suspense fallback="Loading...">
<Button />
</Suspense>
</div>
);
}

const finalHTML = ReactDOMServer.renderToString(<App />);
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, <App />);
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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -186,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;
Expand Down Expand Up @@ -273,6 +274,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
document.body.removeChild(container);
});

// @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
it('hydrates at higher pri for secondary discrete events', async () => {
let suspend = false;
let resolve;
Expand Down Expand Up @@ -437,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;
Expand Down Expand Up @@ -525,6 +528,7 @@ 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;
Expand Down Expand Up @@ -623,6 +627,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
document.body.removeChild(container);
});

// @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
salazarm marked this conversation as resolved.
Show resolved Hide resolved
it('hydrates the hovered targets as higher priority for continuous events', async () => {
let suspend = false;
let resolve;
Expand Down Expand Up @@ -725,6 +730,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
document.body.removeChild(container);
});

// @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
salazarm marked this conversation as resolved.
Show resolved Hide resolved
it('hydrates the last target path first for continuous events', async () => {
let suspend = false;
let resolve;
Expand Down Expand Up @@ -860,6 +866,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
});

// @gate experimental || www
// @gate !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
salazarm marked this conversation as resolved.
Show resolved Hide resolved
it('hydrates before an update even if hydration moves away from it', async () => {
function Child({text}) {
Scheduler.unstable_yieldValue(text);
Expand Down Expand Up @@ -970,4 +977,94 @@ describe('ReactDOMServerSelectiveHydration', () => {

document.body.removeChild(container);
});

// @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
it('Fires capture event handlers and native events if content is hydratable during discrete event', async () => {
salazarm marked this conversation as resolved.
Show resolved Hide resolved
spyOnDev(console, 'error');
function Child({text}) {
Scheduler.unstable_yieldValue(text);
const ref = React.useRef();
React.useLayoutEffect(() => {
if (!ref.current) {
return;
}
ref.current.addEventListener('click', () => {
salazarm marked this conversation as resolved.
Show resolved Hide resolved
Scheduler.unstable_yieldValue('Native Click ' + text);
});
}, [text]);
return (
<span
ref={ref}
onClickCapture={() => {
Scheduler.unstable_yieldValue('Capture Clicked ' + text);
}}
onClick={e => {
Scheduler.unstable_yieldValue('Clicked ' + text);
}}>
{text}
</span>
);
}

function App() {
Scheduler.unstable_yieldValue('App');
return (
<div>
<Suspense fallback="Loading...">
<Child text="A" />
</Suspense>
<Suspense fallback="Loading...">
<Child text="B" />
</Suspense>
</div>
);
}

const finalHTML = ReactDOMServer.renderToString(<App />);
if (__DEV__) {
salazarm marked this conversation as resolved.
Show resolved Hide resolved
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];

ReactDOM.hydrateRoot(container, <App />);

// 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);
if (__DEV__) {
expect(console.error).toHaveBeenCalledTimes(2);
}
});
});
9 changes: 4 additions & 5 deletions packages/react-dom/src/client/ReactDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,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,
Expand Down Expand Up @@ -108,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)) {
salazarm marked this conversation as resolved.
Show resolved Hide resolved
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);
Expand Down
Loading