@samitouri / QOS-React / commits / e98797f7b9

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

Sebastian Markbåge committed Jan 30, 2020 at 09:32 UTC e98797f7b98c827fb691e519bc2c445977a0546c
2 files changed +251 -4
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+243
@@ -9,11 +9,14 @@
9
10 'use strict';
11
12 +import {createEventTarget} from 'dom-event-testing-library';
13 +
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 let Scheduler;
18 let Suspense;
19 +let usePress;
20
21 function dispatchMouseHoverEvent(to, from) {
22 if (!to) {
@@ -92,11 +95,16 @@ describe('ReactDOMServerSelectiveHydration', () => {
95 beforeEach(() => {
96 jest.resetModuleRegistry();
97
98 + let ReactFeatureFlags = require('shared/ReactFeatureFlags');
99 + ReactFeatureFlags.enableDeprecatedFlareAPI = true;
100 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
101 +
102 React = require('react');
103 ReactDOM = require('react-dom');
104 ReactDOMServer = require('react-dom/server');
105 Scheduler = require('scheduler');
106 Suspense = React.Suspense;
107 + usePress = require('react-interactions/events/press').usePress;
108 });
109
110 if (!__EXPERIMENTAL__) {
@@ -342,6 +350,241 @@ describe('ReactDOMServerSelectiveHydration', () => {
350 document.body.removeChild(container);
351 });
352
353 + it('hydrates the target boundary synchronously during a click (flare)', async () => {
354 + function Child({text}) {
355 + Scheduler.unstable_yieldValue(text);
356 + const listener = usePress({
357 + onPress() {
358 + Scheduler.unstable_yieldValue('Clicked ' + text);
359 + },
360 + });
361 +
362 + return <span DEPRECATED_flareListeners={listener}>{text}</span>;
363 + }
364 +
365 + function App() {
366 + Scheduler.unstable_yieldValue('App');
367 + return (
368 + <div>
369 + <Suspense fallback="Loading...">
370 + <Child text="A" />
371 + </Suspense>
372 + <Suspense fallback="Loading...">
373 + <Child text="B" />
374 + </Suspense>
375 + </div>
376 + );
377 + }
378 +
379 + let finalHTML = ReactDOMServer.renderToString(<App />);
380 +
381 + expect(Scheduler).toHaveYielded(['App', 'A', 'B']);
382 +
383 + let container = document.createElement('div');
384 + // We need this to be in the document since we'll dispatch events on it.
385 + document.body.appendChild(container);
386 +
387 + container.innerHTML = finalHTML;
388 +
389 + let root = ReactDOM.createRoot(container, {hydrate: true});
390 + root.render(<App />);
391 +
392 + // Nothing has been hydrated so far.
393 + expect(Scheduler).toHaveYielded([]);
394 +
395 + let span = container.getElementsByTagName('span')[1];
396 +
397 + let target = createEventTarget(span);
398 +
399 + // This should synchronously hydrate the root App and the second suspense
400 + // boundary.
401 + let preventDefault = jest.fn();
402 + target.virtualclick({preventDefault});
403 +
404 + // The event should have been canceled because we called preventDefault.
405 + expect(preventDefault).toHaveBeenCalled();
406 +
407 + // We rendered App, B and then invoked the event without rendering A.
408 + expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']);
409 +
410 + // After continuing the scheduler, we finally hydrate A.
411 + expect(Scheduler).toFlushAndYield(['A']);
412 +
413 + document.body.removeChild(container);
414 + });
415 +
416 + it('hydrates at higher pri if sync did not work first time (flare)', async () => {
417 + let suspend = false;
418 + let resolve;
419 + let promise = new Promise(resolvePromise => (resolve = resolvePromise));
420 +
421 + function Child({text}) {
422 + if ((text === 'A' || text === 'D') && suspend) {
423 + throw promise;
424 + }
425 + Scheduler.unstable_yieldValue(text);
426 +
427 + const listener = usePress({
428 + onPress() {
429 + Scheduler.unstable_yieldValue('Clicked ' + text);
430 + },
431 + });
432 + return <span DEPRECATED_flareListeners={listener}>{text}</span>;
433 + }
434 +
435 + function App() {
436 + Scheduler.unstable_yieldValue('App');
437 + return (
438 + <div>
439 + <Suspense fallback="Loading...">
440 + <Child text="A" />
441 + </Suspense>
442 + <Suspense fallback="Loading...">
443 + <Child text="B" />
444 + </Suspense>
445 + <Suspense fallback="Loading...">
446 + <Child text="C" />
447 + </Suspense>
448 + <Suspense fallback="Loading...">
449 + <Child text="D" />
450 + </Suspense>
451 + </div>
452 + );
453 + }
454 +
455 + let finalHTML = ReactDOMServer.renderToString(<App />);
456 +
457 + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
458 +
459 + let container = document.createElement('div');
460 + // We need this to be in the document since we'll dispatch events on it.
461 + document.body.appendChild(container);
462 +
463 + container.innerHTML = finalHTML;
464 +
465 + let spanD = container.getElementsByTagName('span')[3];
466 +
467 + suspend = true;
468 +
469 + // A and D will be suspended. We'll click on D which should take
470 + // priority, after we unsuspend.
471 + let root = ReactDOM.createRoot(container, {hydrate: true});
472 + root.render(<App />);
473 +
474 + // Nothing has been hydrated so far.
475 + expect(Scheduler).toHaveYielded([]);
476 +
477 + // This click target cannot be hydrated yet because it's suspended.
478 + let result = dispatchClickEvent(spanD);
479 +
480 + expect(Scheduler).toHaveYielded(['App']);
481 +
482 + expect(result).toBe(true);
483 +
484 + // Continuing rendering will render B next.
485 + expect(Scheduler).toFlushAndYield(['B', 'C']);
486 +
487 + suspend = false;
488 + resolve();
489 + await promise;
490 +
491 + // After the click, we should prioritize D and the Click first,
492 + // and only after that render A and C.
493 + expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']);
494 +
495 + document.body.removeChild(container);
496 + });
497 +
498 + it('hydrates at higher pri for secondary discrete events (flare)', async () => {
499 + let suspend = false;
500 + let resolve;
501 + let promise = new Promise(resolvePromise => (resolve = resolvePromise));
502 +
503 + function Child({text}) {
504 + if ((text === 'A' || text === 'D') && suspend) {
505 + throw promise;
506 + }
507 + Scheduler.unstable_yieldValue(text);
508 +
509 + const listener = usePress({
510 + onPress() {
511 + Scheduler.unstable_yieldValue('Clicked ' + text);
512 + },
513 + });
514 + return <span DEPRECATED_flareListeners={listener}>{text}</span>;
515 + }
516 +
517 + function App() {
518 + Scheduler.unstable_yieldValue('App');
519 + return (
520 + <div>
521 + <Suspense fallback="Loading...">
522 + <Child text="A" />
523 + </Suspense>
524 + <Suspense fallback="Loading...">
525 + <Child text="B" />
526 + </Suspense>
527 + <Suspense fallback="Loading...">
528 + <Child text="C" />
529 + </Suspense>
530 + <Suspense fallback="Loading...">
531 + <Child text="D" />
532 + </Suspense>
533 + </div>
534 + );
535 + }
536 +
537 + let finalHTML = ReactDOMServer.renderToString(<App />);
538 +
539 + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
540 +
541 + let container = document.createElement('div');
542 + // We need this to be in the document since we'll dispatch events on it.
543 + document.body.appendChild(container);
544 +
545 + container.innerHTML = finalHTML;
546 +
547 + let spanA = container.getElementsByTagName('span')[0];
548 + let spanC = container.getElementsByTagName('span')[2];
549 + let spanD = container.getElementsByTagName('span')[3];
550 +
551 + suspend = true;
552 +
553 + // A and D will be suspended. We'll click on D which should take
554 + // priority, after we unsuspend.
555 + let root = ReactDOM.createRoot(container, {hydrate: true});
556 + root.render(<App />);
557 +
558 + // Nothing has been hydrated so far.
559 + expect(Scheduler).toHaveYielded([]);
560 +
561 + // This click target cannot be hydrated yet because the first is Suspended.
562 + dispatchClickEvent(spanA);
563 + dispatchClickEvent(spanC);
564 + dispatchClickEvent(spanD);
565 +
566 + expect(Scheduler).toHaveYielded(['App']);
567 +
568 + suspend = false;
569 + resolve();
570 + await promise;
571 +
572 + // We should prioritize hydrating A, C and D first since we clicked in
573 + // them. Only after they're done will we hydrate B.
574 + expect(Scheduler).toFlushAndYield([
575 + 'A',
576 + 'Clicked A',
577 + 'C',
578 + 'Clicked C',
579 + 'D',
580 + 'Clicked D',
581 + // B should render last since it wasn't clicked.
582 + 'B',
583 + ]);
584 +
585 + document.body.removeChild(container);
586 + });
587 +
588 it('hydrates the last target as higher priority for continuous events', async () => {
589 let suspend = false;
590 let resolve;
packages/react-dom/src/events/ReactDOMEventReplaying.js
+8 -4
@@ -221,14 +221,18 @@ function trapReplayableEvent(
221 if (enableDeprecatedFlareAPI) {
222 // Trap events for the responder system.
223 const topLevelTypeString = unsafeCastDOMTopLevelTypeToString(topLevelType);
224 - const passiveEventKey = topLevelTypeString + '_passive';
225 - if (!listenerMap.has(passiveEventKey)) {
224 + // TODO: Ideally we shouldn't need these to be active but
225 + // if we only have a passive listener, we at least need it
226 + // to still pretend to be active so that Flare gets those
227 + // events.
228 + const activeEventKey = topLevelTypeString + '_active';
229 + if (!listenerMap.has(activeEventKey)) {
230 const listener = addResponderEventSystemEvent(
231 document,
232 topLevelTypeString,
229 - true,
233 + false,
234 );
231 - listenerMap.set(passiveEventKey, listener);
235 + listenerMap.set(activeEventKey, listener);
236 }
237 }
238 }