mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Fix Event Replaying in Flare by Eagerly Adding Active Listeners (#17933)
* Add test of Event Replaying using Flare * Fix Event Replaying in Flare by Eagerly Adding Active Listeners This effectively reverts part of https://github.com/facebook/react/pull/17513
This commit is contained in:
@@ -9,11 +9,14 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import {createEventTarget} from 'dom-event-testing-library';
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let Scheduler;
|
||||
let Suspense;
|
||||
let usePress;
|
||||
|
||||
function dispatchMouseHoverEvent(to, from) {
|
||||
if (!to) {
|
||||
@@ -92,11 +95,16 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModuleRegistry();
|
||||
|
||||
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
Scheduler = require('scheduler');
|
||||
Suspense = React.Suspense;
|
||||
usePress = require('react-interactions/events/press').usePress;
|
||||
});
|
||||
|
||||
if (!__EXPERIMENTAL__) {
|
||||
@@ -342,6 +350,241 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
it('hydrates the target boundary synchronously during a click (flare)', async () => {
|
||||
function Child({text}) {
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
const listener = usePress({
|
||||
onPress() {
|
||||
Scheduler.unstable_yieldValue('Clicked ' + text);
|
||||
},
|
||||
});
|
||||
|
||||
return <span DEPRECATED_flareListeners={listener}>{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>
|
||||
);
|
||||
}
|
||||
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['App', 'A', 'B']);
|
||||
|
||||
let 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;
|
||||
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// Nothing has been hydrated so far.
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
|
||||
let span = container.getElementsByTagName('span')[1];
|
||||
|
||||
let target = createEventTarget(span);
|
||||
|
||||
// This should synchronously hydrate the root App and the second suspense
|
||||
// boundary.
|
||||
let preventDefault = jest.fn();
|
||||
target.virtualclick({preventDefault});
|
||||
|
||||
// The event should have been canceled because we called preventDefault.
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
|
||||
// We rendered App, B and then invoked the event without rendering A.
|
||||
expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']);
|
||||
|
||||
// After continuing the scheduler, we finally hydrate A.
|
||||
expect(Scheduler).toFlushAndYield(['A']);
|
||||
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
it('hydrates at higher pri if sync did not work first time (flare)', async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
|
||||
function Child({text}) {
|
||||
if ((text === 'A' || text === 'D') && suspend) {
|
||||
throw promise;
|
||||
}
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
|
||||
const listener = usePress({
|
||||
onPress() {
|
||||
Scheduler.unstable_yieldValue('Clicked ' + text);
|
||||
},
|
||||
});
|
||||
return <span DEPRECATED_flareListeners={listener}>{text}</span>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
Scheduler.unstable_yieldValue('App');
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child text="A" />
|
||||
</Suspense>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child text="B" />
|
||||
</Suspense>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child text="C" />
|
||||
</Suspense>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child text="D" />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
|
||||
|
||||
let 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;
|
||||
|
||||
let spanD = container.getElementsByTagName('span')[3];
|
||||
|
||||
suspend = true;
|
||||
|
||||
// A and D will be suspended. We'll click on D which should take
|
||||
// priority, after we unsuspend.
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// Nothing has been hydrated so far.
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
|
||||
// This click target cannot be hydrated yet because it's suspended.
|
||||
let result = dispatchClickEvent(spanD);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['App']);
|
||||
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Continuing rendering will render B next.
|
||||
expect(Scheduler).toFlushAndYield(['B', 'C']);
|
||||
|
||||
suspend = false;
|
||||
resolve();
|
||||
await promise;
|
||||
|
||||
// After the click, we should prioritize D and the Click first,
|
||||
// and only after that render A and C.
|
||||
expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']);
|
||||
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
it('hydrates at higher pri for secondary discrete events (flare)', async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
|
||||
function Child({text}) {
|
||||
if ((text === 'A' || text === 'D') && suspend) {
|
||||
throw promise;
|
||||
}
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
|
||||
const listener = usePress({
|
||||
onPress() {
|
||||
Scheduler.unstable_yieldValue('Clicked ' + text);
|
||||
},
|
||||
});
|
||||
return <span DEPRECATED_flareListeners={listener}>{text}</span>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
Scheduler.unstable_yieldValue('App');
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child text="A" />
|
||||
</Suspense>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child text="B" />
|
||||
</Suspense>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child text="C" />
|
||||
</Suspense>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child text="D" />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
|
||||
|
||||
let 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;
|
||||
|
||||
let spanA = container.getElementsByTagName('span')[0];
|
||||
let spanC = container.getElementsByTagName('span')[2];
|
||||
let spanD = container.getElementsByTagName('span')[3];
|
||||
|
||||
suspend = true;
|
||||
|
||||
// A and D will be suspended. We'll click on D which should take
|
||||
// priority, after we unsuspend.
|
||||
let root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
|
||||
// Nothing has been hydrated so far.
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
|
||||
// This click target cannot be hydrated yet because the first is Suspended.
|
||||
dispatchClickEvent(spanA);
|
||||
dispatchClickEvent(spanC);
|
||||
dispatchClickEvent(spanD);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['App']);
|
||||
|
||||
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).toFlushAndYield([
|
||||
'A',
|
||||
'Clicked A',
|
||||
'C',
|
||||
'Clicked C',
|
||||
'D',
|
||||
'Clicked D',
|
||||
// B should render last since it wasn't clicked.
|
||||
'B',
|
||||
]);
|
||||
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
it('hydrates the last target as higher priority for continuous events', async () => {
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
|
||||
+8
-4
@@ -221,14 +221,18 @@ function trapReplayableEvent(
|
||||
if (enableDeprecatedFlareAPI) {
|
||||
// Trap events for the responder system.
|
||||
const topLevelTypeString = unsafeCastDOMTopLevelTypeToString(topLevelType);
|
||||
const passiveEventKey = topLevelTypeString + '_passive';
|
||||
if (!listenerMap.has(passiveEventKey)) {
|
||||
// TODO: Ideally we shouldn't need these to be active but
|
||||
// if we only have a passive listener, we at least need it
|
||||
// to still pretend to be active so that Flare gets those
|
||||
// events.
|
||||
const activeEventKey = topLevelTypeString + '_active';
|
||||
if (!listenerMap.has(activeEventKey)) {
|
||||
const listener = addResponderEventSystemEvent(
|
||||
document,
|
||||
topLevelTypeString,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
listenerMap.set(passiveEventKey, listener);
|
||||
listenerMap.set(activeEventKey, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user