@samitouri / QOS-React-2 / commits / 0a527707cd

Event Replaying (#16725)

* Add Event Replaying Infra * Wire up Roots and Suspense boundaries, to retry events, after they commit * Replay discrete events in order in a separate scheduler callback * Add continuous events These events only replay their last target if the target is not yet hydrated. That way we don't have to wait for a previously hovered boundary before invoking the current target. * Enable tests from before These tests were written with replaying in mind and now we can properly enable them. * Unify replaying and dispatching * Mark system flags as a replay and pass to legacy events That way we can check if this is a replay and therefore needs a special case. One such special case is "mouseover" where we check the relatedTarget. * Eagerly listen to all replayable events To minimize breakages in a minor, I only do this for the new root APIs since replaying only matters there anyway. Only if hydrating. For Flare, I have to attach all active listeners since the current system has one DOM listener for each. In a follow up I plan on optimizing that by only attaching one if there's at least one active listener which would allow us to start with only passive and then upgrade. * Desperate attempt to save bytese * Add test for mouseover replaying We need to check if the "relatedTarget" is mounted due to how the old event system dispatches from the "out" event. * Fix for nested boundaries and suspense in root container This is a follow up to #16673 which didn't have a test because it wasn't observable yet. This shows that it had a bug. * Rename RESPONDER_EVENT_SYSTEM to PLUGIN_EVENT_SYSTEM

Sebastian Markbåge committed Sep 23, 2019 at 11:21 UTC 0a527707cd8befd21a741ca9646a8551842190b0
25 files changed +1270 -131
packages/legacy-events/EventPluginHub.js
+5
@@ -22,6 +22,7 @@ import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
22 import type {Fiber} from 'react-reconciler/src/ReactFiber';
23 import type {AnyNativeEvent} from './PluginModuleType';
24 import type {TopLevelType} from './TopLevelEventTypes';
25 +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
26
27 function isInteractive(tag) {
28 return (
@@ -131,6 +132,7 @@ export function getListener(inst: Fiber, registrationName: string) {
132 */
133 function extractPluginEvents(
134 topLevelType: TopLevelType,
135 + eventSystemFlags: EventSystemFlags,
136 targetInst: null | Fiber,
137 nativeEvent: AnyNativeEvent,
138 nativeEventTarget: EventTarget,
@@ -142,6 +144,7 @@ function extractPluginEvents(
144 if (possiblePlugin) {
145 const extractedEvents = possiblePlugin.extractEvents(
146 topLevelType,
147 + eventSystemFlags,
148 targetInst,
149 nativeEvent,
150 nativeEventTarget,
@@ -156,12 +159,14 @@ function extractPluginEvents(
159
160 export function runExtractedPluginEventsInBatch(
161 topLevelType: TopLevelType,
162 + eventSystemFlags: EventSystemFlags,
163 targetInst: null | Fiber,
164 nativeEvent: AnyNativeEvent,
165 nativeEventTarget: EventTarget,
166 ) {
167 const events = extractPluginEvents(
168 topLevelType,
169 + eventSystemFlags,
170 targetInst,
171 nativeEvent,
172 nativeEventTarget,
packages/legacy-events/EventSystemFlags.js
+1
@@ -14,3 +14,4 @@ export const RESPONDER_EVENT_SYSTEM = 1 << 1;
14 export const IS_PASSIVE = 1 << 2;
15 export const IS_ACTIVE = 1 << 3;
16 export const PASSIVE_NOT_SUPPORTED = 1 << 4;
17 +export const IS_REPLAYED = 1 << 5;
packages/legacy-events/PluginModuleType.js
+2
@@ -13,6 +13,7 @@ import type {
13 ReactSyntheticEvent,
14 } from './ReactSyntheticEventType';
15 import type {TopLevelType} from './TopLevelEventTypes';
16 +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
17
18 export type EventTypes = {[key: string]: DispatchConfig};
19
@@ -24,6 +25,7 @@ export type PluginModule<NativeEvent> = {
25 eventTypes: EventTypes,
26 extractEvents: (
27 topLevelType: TopLevelType,
28 + eventSystemFlags: EventSystemFlags,
29 targetInst: null | Fiber,
30 nativeTarget: NativeEvent,
31 nativeEventTarget: EventTarget,
packages/legacy-events/ResponderEventPlugin.js
+1
@@ -504,6 +504,7 @@ const ResponderEventPlugin = {
504 */
505 extractEvents: function(
506 topLevelType,
507 + eventSystemFlags,
508 targetInst,
509 nativeEvent,
510 nativeEventTarget,
packages/legacy-events/__tests__/ResponderEventPlugin-test.internal.js
+2
@@ -10,6 +10,7 @@
10 'use strict';
11
12 const {HostComponent} = require('shared/ReactWorkTags');
13 +const {PLUGIN_EVENT_SYSTEM} = require('legacy-events/EventSystemFlags');
14
15 let EventBatching;
16 let EventPluginUtils;
@@ -313,6 +314,7 @@ const run = function(config, hierarchyConfig, nativeEventConfig) {
314 // Trigger the event
315 const extractedEvents = ResponderEventPlugin.extractEvents(
316 nativeEventConfig.topLevelType,
317 + PLUGIN_EVENT_SYSTEM,
318 nativeEventConfig.targetInst,
319 nativeEventConfig.nativeEvent,
320 nativeEventConfig.target,
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+436 -22
@@ -17,6 +17,58 @@ let ReactFeatureFlags;
17 let Suspense;
18 let SuspenseList;
19 let act;
20 +let useHover;
21 +
22 +function dispatchMouseEvent(to, from) {
23 + if (!to) {
24 + to = null;
25 + }
26 + if (!from) {
27 + from = null;
28 + }
29 + if (from) {
30 + const mouseOutEvent = document.createEvent('MouseEvents');
31 + mouseOutEvent.initMouseEvent(
32 + 'mouseout',
33 + true,
34 + true,
35 + window,
36 + 0,
37 + 50,
38 + 50,
39 + 50,
40 + 50,
41 + false,
42 + false,
43 + false,
44 + false,
45 + 0,
46 + to,
47 + );
48 + from.dispatchEvent(mouseOutEvent);
49 + }
50 + if (to) {
51 + const mouseOverEvent = document.createEvent('MouseEvents');
52 + mouseOverEvent.initMouseEvent(
53 + 'mouseover',
54 + true,
55 + true,
56 + window,
57 + 0,
58 + 50,
59 + 50,
60 + 50,
61 + 50,
62 + false,
63 + false,
64 + false,
65 + false,
66 + 0,
67 + from,
68 + );
69 + to.dispatchEvent(mouseOverEvent);
70 + }
71 +}
72
73 describe('ReactDOMServerPartialHydration', () => {
74 beforeEach(() => {
@@ -34,6 +86,8 @@ describe('ReactDOMServerPartialHydration', () => {
86 Scheduler = require('scheduler');
87 Suspense = React.Suspense;
88 SuspenseList = React.unstable_SuspenseList;
89 +
90 + useHover = require('react-interactions/events/hover').useHover;
91 });
92
93 it('hydrates a parent even if a child Suspense boundary is blocked', async () => {
@@ -1805,12 +1859,6 @@ describe('ReactDOMServerPartialHydration', () => {
1859 Scheduler.unstable_flushAll();
1860 jest.runAllTimers();
1861
1808 - // TODO: With selective hydration the event should've been replayed
1809 - // but for now we'll have to issue it again.
1810 - act(() => {
1811 - a.click();
1812 - });
1813 -
1862 expect(clicks).toBe(1);
1863
1864 expect(container.textContent).toBe('Hello');
@@ -1868,6 +1916,12 @@ describe('ReactDOMServerPartialHydration', () => {
1916 suspend = true;
1917 let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1918 root.render(<App />);
1919 +
1920 + // We'll do one click before hydrating.
1921 + a.click();
1922 + // This should be delayed.
1923 + expect(onEvent).toHaveBeenCalledTimes(0);
1924 +
1925 Scheduler.unstable_flushAll();
1926 jest.runAllTimers();
1927
@@ -1885,13 +1939,165 @@ describe('ReactDOMServerPartialHydration', () => {
1939 Scheduler.unstable_flushAll();
1940 jest.runAllTimers();
1941
1888 - // TODO: With selective hydration the event should've been replayed
1889 - // but for now we'll have to issue it again.
1890 - act(() => {
1891 - a.click();
1942 + expect(onEvent).toHaveBeenCalledTimes(2);
1943 +
1944 + document.body.removeChild(container);
1945 + });
1946 +
1947 + it('invokes discrete events on nested suspense boundaries in a root (legacy system)', async () => {
1948 + let suspend = false;
1949 + let resolve;
1950 + let promise = new Promise(resolvePromise => (resolve = resolvePromise));
1951 +
1952 + let clicks = 0;
1953 +
1954 + function Button() {
1955 + return (
1956 + <a
1957 + onClick={() => {
1958 + clicks++;
1959 + }}>
1960 + Click me
1961 + </a>
1962 + );
1963 + }
1964 +
1965 + function Child() {
1966 + if (suspend) {
1967 + throw promise;
1968 + } else {
1969 + return (
1970 + <Suspense fallback="Loading...">
1971 + <Button />
1972 + </Suspense>
1973 + );
1974 + }
1975 + }
1976 +
1977 + function App() {
1978 + return (
1979 + <Suspense fallback="Loading...">
1980 + <Child />
1981 + </Suspense>
1982 + );
1983 + }
1984 +
1985 + suspend = false;
1986 + let finalHTML = ReactDOMServer.renderToString(<App />);
1987 + let container = document.createElement('div');
1988 + container.innerHTML = finalHTML;
1989 +
1990 + // We need this to be in the document since we'll dispatch events on it.
1991 + document.body.appendChild(container);
1992 +
1993 + let a = container.getElementsByTagName('a')[0];
1994 +
1995 + // On the client we don't have all data yet but we want to start
1996 + // hydrating anyway.
1997 + suspend = true;
1998 + let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1999 + root.render(<App />);
2000 +
2001 + // We'll do one click before hydrating.
2002 + a.click();
2003 + // This should be delayed.
2004 + expect(clicks).toBe(0);
2005 +
2006 + Scheduler.unstable_flushAll();
2007 + jest.runAllTimers();
2008 +
2009 + // We're now partially hydrated.
2010 + a.click();
2011 + expect(clicks).toBe(0);
2012 +
2013 + // Resolving the promise so that rendering can complete.
2014 + suspend = false;
2015 + resolve();
2016 + await promise;
2017 +
2018 + Scheduler.unstable_flushAll();
2019 + jest.runAllTimers();
2020 +
2021 + expect(clicks).toBe(2);
2022 +
2023 + document.body.removeChild(container);
2024 + });
2025 +
2026 + it('invokes discrete events on nested suspense boundaries in a root (responder system)', async () => {
2027 + let suspend = false;
2028 + let resolve;
2029 + let promise = new Promise(resolvePromise => (resolve = resolvePromise));
2030 +
2031 + const onEvent = jest.fn();
2032 + const TestResponder = React.unstable_createResponder('TestEventResponder', {
2033 + targetEventTypes: ['click'],
2034 + onEvent,
2035 });
2036
1894 - expect(onEvent).toHaveBeenCalledTimes(1);
2037 + function Button() {
2038 + let listener = React.unstable_useResponder(TestResponder, {});
2039 + return <a listeners={listener}>Click me</a>;
2040 + }
2041 +
2042 + function Child() {
2043 + if (suspend) {
2044 + throw promise;
2045 + } else {
2046 + return (
2047 + <Suspense fallback="Loading...">
2048 + <Button />
2049 + </Suspense>
2050 + );
2051 + }
2052 + }
2053 +
2054 + function App() {
2055 + return (
2056 + <Suspense fallback="Loading...">
2057 + <Child />
2058 + </Suspense>
2059 + );
2060 + }
2061 +
2062 + suspend = false;
2063 + let finalHTML = ReactDOMServer.renderToString(<App />);
2064 + let container = document.createElement('div');
2065 + container.innerHTML = finalHTML;
2066 +
2067 + // We need this to be in the document since we'll dispatch events on it.
2068 + document.body.appendChild(container);
2069 +
2070 + let a = container.getElementsByTagName('a')[0];
2071 +
2072 + // On the client we don't have all data yet but we want to start
2073 + // hydrating anyway.
2074 + suspend = true;
2075 + let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
2076 + root.render(<App />);
2077 +
2078 + // We'll do one click before hydrating.
2079 + a.click();
2080 + // This should be delayed.
2081 + expect(onEvent).toHaveBeenCalledTimes(0);
2082 +
2083 + Scheduler.unstable_flushAll();
2084 + jest.runAllTimers();
2085 +
2086 + // We're now partially hydrated.
2087 + a.click();
2088 + // We should not have invoked the event yet because we're not
2089 + // yet hydrated.
2090 + expect(onEvent).toHaveBeenCalledTimes(0);
2091 +
2092 + // Resolving the promise so that rendering can complete.
2093 + suspend = false;
2094 + resolve();
2095 + await promise;
2096 +
2097 + Scheduler.unstable_flushAll();
2098 + jest.runAllTimers();
2099 +
2100 + expect(onEvent).toHaveBeenCalledTimes(2);
2101
2102 document.body.removeChild(container);
2103 });
@@ -1963,12 +2169,6 @@ describe('ReactDOMServerPartialHydration', () => {
2169 Scheduler.unstable_flushAll();
2170 jest.runAllTimers();
2171
1966 - // TODO: With selective hydration the event should've been replayed
1967 - // but for now we'll have to issue it again.
1968 - act(() => {
1969 - span.click();
1970 - });
1971 -
2172 expect(clicksOnChild).toBe(1);
2173 // This will be zero due to the stopPropagation.
2174 expect(clicksOnParent).toBe(0);
@@ -2047,14 +2247,228 @@ describe('ReactDOMServerPartialHydration', () => {
2247 jest.runAllTimers();
2248
2249 // We're now full hydrated.
2050 - // TODO: With selective hydration the event should've been replayed
2051 - // but for now we'll have to issue it again.
2052 - act(() => {
2053 - a.click();
2054 - });
2250
2251 expect(clicks).toBe(1);
2252
2253 document.body.removeChild(parentContainer);
2254 });
2255 +
2256 + it('blocks only on the last continuous event (legacy system)', async () => {
2257 + let suspend1 = false;
2258 + let resolve1;
2259 + let promise1 = new Promise(resolvePromise => (resolve1 = resolvePromise));
2260 + let suspend2 = false;
2261 + let resolve2;
2262 + let promise2 = new Promise(resolvePromise => (resolve2 = resolvePromise));
2263 +
2264 + function First({text}) {
2265 + if (suspend1) {
2266 + throw promise1;
2267 + } else {
2268 + return 'Hello';
2269 + }
2270 + }
2271 +
2272 + function Second({text}) {
2273 + if (suspend2) {
2274 + throw promise2;
2275 + } else {
2276 + return 'World';
2277 + }
2278 + }
2279 +
2280 + let ops = [];
2281 +
2282 + function App() {
2283 + return (
2284 + <div>
2285 + <Suspense fallback="Loading First...">
2286 + <span
2287 + onMouseEnter={() => ops.push('Mouse Enter First')}
2288 + onMouseLeave={() => ops.push('Mouse Leave First')}
2289 + />
2290 + {/* We suspend after to test what happens when we eager
2291 + attach the listener. */}
2292 + <First />
2293 + </Suspense>
2294 + <Suspense fallback="Loading Second...">
2295 + <span
2296 + onMouseEnter={() => ops.push('Mouse Enter Second')}
2297 + onMouseLeave={() => ops.push('Mouse Leave Second')}>
2298 + <Second />
2299 + </span>
2300 + </Suspense>
2301 + </div>
2302 + );
2303 + }
2304 +
2305 + let finalHTML = ReactDOMServer.renderToString(<App />);
2306 + let container = document.createElement('div');
2307 + container.innerHTML = finalHTML;
2308 +
2309 + // We need this to be in the document since we'll dispatch events on it.
2310 + document.body.appendChild(container);
2311 +
2312 + let appDiv = container.getElementsByTagName('div')[0];
2313 + let firstSpan = appDiv.getElementsByTagName('span')[0];
2314 + let secondSpan = appDiv.getElementsByTagName('span')[1];
2315 + expect(firstSpan.textContent).toBe('');
2316 + expect(secondSpan.textContent).toBe('World');
2317 +
2318 + // On the client we don't have all data yet but we want to start
2319 + // hydrating anyway.
2320 + suspend1 = true;
2321 + suspend2 = true;
2322 + let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
2323 + root.render(<App />);
2324 +
2325 + Scheduler.unstable_flushAll();
2326 + jest.runAllTimers();
2327 +
2328 + dispatchMouseEvent(appDiv, null);
2329 + dispatchMouseEvent(firstSpan, appDiv);
2330 + dispatchMouseEvent(secondSpan, firstSpan);
2331 +
2332 + // Neither target is yet hydrated.
2333 + expect(ops).toEqual([]);
2334 +
2335 + // Resolving the second promise so that rendering can complete.
2336 + suspend2 = false;
2337 + resolve2();
2338 + await promise2;
2339 +
2340 + Scheduler.unstable_flushAll();
2341 + jest.runAllTimers();
2342 +
2343 + // We've unblocked the current hover target so we should be
2344 + // able to replay it now.
2345 + expect(ops).toEqual(['Mouse Enter Second']);
2346 +
2347 + // Resolving the first promise has no effect now.
2348 + suspend1 = false;
2349 + resolve1();
2350 + await promise1;
2351 +
2352 + Scheduler.unstable_flushAll();
2353 + jest.runAllTimers();
2354 +
2355 + expect(ops).toEqual(['Mouse Enter Second']);
2356 +
2357 + document.body.removeChild(container);
2358 + });
2359 +
2360 + it('blocks only on the last continuous event (Responder system)', async () => {
2361 + let suspend1 = false;
2362 + let resolve1;
2363 + let promise1 = new Promise(resolvePromise => (resolve1 = resolvePromise));
2364 + let suspend2 = false;
2365 + let resolve2;
2366 + let promise2 = new Promise(resolvePromise => (resolve2 = resolvePromise));
2367 +
2368 + function First({text}) {
2369 + if (suspend1) {
2370 + throw promise1;
2371 + } else {
2372 + return 'Hello';
2373 + }
2374 + }
2375 +
2376 + function Second({text}) {
2377 + if (suspend2) {
2378 + throw promise2;
2379 + } else {
2380 + return 'World';
2381 + }
2382 + }
2383 +
2384 + let ops = [];
2385 +
2386 + function App() {
2387 + const listener1 = useHover({
2388 + onHoverStart() {
2389 + ops.push('Hover Start First');
2390 + },
2391 + onHoverEnd() {
2392 + ops.push('Hover End First');
2393 + },
2394 + });
2395 + const listener2 = useHover({
2396 + onHoverStart() {
2397 + ops.push('Hover Start Second');
2398 + },
2399 + onHoverEnd() {
2400 + ops.push('Hover End Second');
2401 + },
2402 + });
2403 + return (
2404 + <div>
2405 + <Suspense fallback="Loading First...">
2406 + <span listeners={listener1} />
2407 + {/* We suspend after to test what happens when we eager
2408 + attach the listener. */}
2409 + <First />
2410 + </Suspense>
2411 + <Suspense fallback="Loading Second...">
2412 + <span listeners={listener2}>
2413 + <Second />
2414 + </span>
2415 + </Suspense>
2416 + </div>
2417 + );
2418 + }
2419 +
2420 + let finalHTML = ReactDOMServer.renderToString(<App />);
2421 + let container = document.createElement('div');
2422 + container.innerHTML = finalHTML;
2423 +
2424 + // We need this to be in the document since we'll dispatch events on it.
2425 + document.body.appendChild(container);
2426 +
2427 + let appDiv = container.getElementsByTagName('div')[0];
2428 + let firstSpan = appDiv.getElementsByTagName('span')[0];
2429 + let secondSpan = appDiv.getElementsByTagName('span')[1];
2430 + expect(firstSpan.textContent).toBe('');
2431 + expect(secondSpan.textContent).toBe('World');
2432 +
2433 + // On the client we don't have all data yet but we want to start
2434 + // hydrating anyway.
2435 + suspend1 = true;
2436 + suspend2 = true;
2437 + let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
2438 + root.render(<App />);
2439 +
2440 + Scheduler.unstable_flushAll();
2441 + jest.runAllTimers();
2442 +
2443 + dispatchMouseEvent(appDiv, null);
2444 + dispatchMouseEvent(firstSpan, appDiv);
2445 + dispatchMouseEvent(secondSpan, firstSpan);
2446 +
2447 + // Neither target is yet hydrated.
2448 + expect(ops).toEqual([]);
2449 +
2450 + // Resolving the second promise so that rendering can complete.
2451 + suspend2 = false;
2452 + resolve2();
2453 + await promise2;
2454 +
2455 + Scheduler.unstable_flushAll();
2456 + jest.runAllTimers();
2457 +
2458 + // We've unblocked the current hover target so we should be
2459 + // able to replay it now.
2460 + expect(ops).toEqual(['Hover Start Second']);
2461 +
2462 + // Resolving the first promise has no effect now.
2463 + suspend1 = false;
2464 + resolve1();
2465 + await promise1;
2466 +
2467 + Scheduler.unstable_flushAll();
2468 + jest.runAllTimers();
2469 +
2470 + expect(ops).toEqual(['Hover Start Second']);
2471 +
2472 + document.body.removeChild(container);
2473 + });
2474 });
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+15 -15
@@ -13,7 +13,6 @@ let React;
13 let ReactDOM;
14 let ReactDOMServer;
15 let Scheduler;
16 -let act;
16
17 // These tests rely both on ReactDOMServer and ReactDOM.
18 // If a test only needs ReactDOMServer, put it in ReactServerRendering-test instead.
@@ -24,7 +23,6 @@ describe('ReactDOMServerHydration', () => {
23 ReactDOM = require('react-dom');
24 ReactDOMServer = require('react-dom/server');
25 Scheduler = require('scheduler');
27 - act = require('react-dom/test-utils').act;
26 });
27
28 it('should have the correct mounting behavior (old hydrate API)', () => {
@@ -570,6 +568,13 @@ describe('ReactDOMServerHydration', () => {
568 // Hydrate asynchronously.
569 let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
570 root.render(<App />);
571 +
572 + // We haven't started hydrating yet.
573 + a.click();
574 + // Clicking should not invoke the event yet because we haven't committed
575 + // the hydration yet.
576 + expect(clicks).toBe(0);
577 +
578 // Flush part way through the render.
579 if (__DEV__) {
580 // In DEV effects gets double invoked.
@@ -587,15 +592,15 @@ describe('ReactDOMServerHydration', () => {
592 expect(clicks).toBe(0);
593
594 // Finish the rest of the hydration.
590 - expect(Scheduler).toFlushAndYield(['Sibling2']);
591 -
592 - // TODO: With selective hydration the event should've been replayed
593 - // but for now we'll have to issue it again.
594 - act(() => {
595 - a.click();
596 - });
595 + if (__DEV__) {
596 + // In DEV effects gets double invoked.
597 + expect(Scheduler).toFlushAndYield(['Sibling2', 'Button', 'Button']);
598 + } else {
599 + expect(Scheduler).toFlushAndYield(['Sibling2', 'Button']);
600 + }
601
598 - expect(clicks).toBe(1);
602 + // We should have picked up both events now.
603 + expect(clicks).toBe(2);
604
605 expect(container.textContent).toBe('Sibling');
606
@@ -649,11 +654,6 @@ describe('ReactDOMServerHydration', () => {
654 Scheduler.unstable_flushAll();
655
656 // We're now full hydrated.
652 - // TODO: With selective hydration the event should've been replayed
653 - // but for now we'll have to issue it again.
654 - act(() => {
655 - a.click();
656 - });
657
658 expect(clicks).toBe(1);
659
packages/react-dom/src/client/ReactDOM.js
+19 -13
@@ -74,6 +74,7 @@ import {
74 } from './ReactDOMComponentTree';
75 import {restoreControlledState} from './ReactDOMComponent';
76 import {dispatchEvent} from '../events/ReactDOMEventListener';
77 +import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
78 import {
79 ELEMENT_NODE,
80 COMMENT_NODE,
@@ -365,7 +366,7 @@ ReactWork.prototype._onCommit = function(): void {
366 }
367 };
368
368 -function ReactSyncRoot(
369 +function createRootImpl(
370 container: DOMContainer,
371 tag: RootTag,
372 options: void | RootOptions,
@@ -375,22 +376,27 @@ function ReactSyncRoot(
376 const hydrationCallbacks =
377 (options != null && options.hydrationOptions) || null;
378 const root = createContainer(container, tag, hydrate, hydrationCallbacks);
378 - this._internalRoot = root;
379 markContainerAsRoot(root.current, container);
380 + if (hydrate && tag !== LegacyRoot) {
381 + const doc =
382 + container.nodeType === DOCUMENT_NODE
383 + ? container
384 + : container.ownerDocument;
385 + eagerlyTrapReplayableEvents(doc);
386 + }
387 + return root;
388 +}
389 +
390 +function ReactSyncRoot(
391 + container: DOMContainer,
392 + tag: RootTag,
393 + options: void | RootOptions,
394 +) {
395 + this._internalRoot = createRootImpl(container, tag, options);
396 }
397
398 function ReactRoot(container: DOMContainer, options: void | RootOptions) {
383 - const hydrate = options != null && options.hydrate === true;
384 - const hydrationCallbacks =
385 - (options != null && options.hydrationOptions) || null;
386 - const root = createContainer(
387 - container,
388 - ConcurrentRoot,
389 - hydrate,
390 - hydrationCallbacks,
391 - );
392 - this._internalRoot = root;
393 - markContainerAsRoot(root.current, container);
399 + this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
400 }
401
402 ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
packages/react-dom/src/client/ReactDOMComponentTree.js
+19 -9
@@ -62,14 +62,21 @@ export function getClosestInstanceFromNode(targetNode) {
62 // a nested suspense boundary within it. So we can use this as a fast
63 // bailout. Most of the time, when people add non-React children to
64 // the tree, it is using a ref to a child-less DOM node.
65 - // We only need to check one of the fibers because if it has ever
66 - // gone from having children to deleting them or vice versa it would
67 - // have deleted the dehydrated boundary nested inside already.
68 - if (targetInst.child !== null) {
65 + // Normally we'd only need to check one of the fibers because if it
66 + // has ever gone from having children to deleting them or vice versa
67 + // it would have deleted the dehydrated boundary nested inside already.
68 + // However, since the HostRoot starts out with an alternate it might
69 + // have one on the alternate so we need to check in case this was a
70 + // root.
71 + const alternate = targetInst.alternate;
72 + if (
73 + targetInst.child !== null ||
74 + (alternate !== null && alternate.child !== null)
75 + ) {
76 // Next we need to figure out if the node that skipped past is
77 // nested within a dehydrated boundary and if so, which one.
78 let suspenseInstance = getParentSuspenseInstance(targetNode);
72 - if (suspenseInstance !== null) {
79 + while (suspenseInstance !== null) {
80 // We found a suspense instance. That means that we haven't
81 // hydrated it yet. Even though we leave the comments in the
82 // DOM after hydrating, and there are boundaries in the DOM
@@ -83,10 +90,13 @@ export function getClosestInstanceFromNode(targetNode) {
90 return targetSuspenseInst;
91 }
92 // If we don't find a Fiber on the comment, it might be because
86 - // we haven't gotten to hydrate it yet. That should mean that
87 - // the parent component also hasn't hydrated yet but we can
88 - // just return that since it will bail out on the isMounted
89 - // check.
93 + // we haven't gotten to hydrate it yet. There might still be a
94 + // parent boundary that hasn't above this one so we need to find
95 + // the outer most that is known.
96 + suspenseInstance = getParentSuspenseInstance(suspenseInstance);
97 + // If we don't find one, then that should mean that the parent
98 + // host component also hasn't hydrated yet. We can return it
99 + // below since it will bail out on the isMounted check later.
100 }
101 }
102 return targetInst;
packages/react-dom/src/client/ReactDOMHostConfig.js
+19
@@ -52,6 +52,7 @@ import {
52 mountEventResponder,
53 unmountEventResponder,
54 } from '../events/DOMEventResponderSystem';
55 +import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';
56
57 export type Type = string;
58 export type Props = {
@@ -477,6 +478,8 @@ export function clearSuspenseBoundary(
478 if (data === SUSPENSE_END_DATA) {
479 if (depth === 0) {
480 parentInstance.removeChild(nextNode);
481 + // Retry if any event replaying was blocked on this.
482 + retryIfBlockedOn(suspenseInstance);
483 return;
484 } else {
485 depth--;
@@ -492,6 +495,8 @@ export function clearSuspenseBoundary(
495 node = nextNode;
496 } while (node);
497 // TODO: Warn, we didn't find the end comment boundary.
498 + // Retry if any event replaying was blocked on this.
499 + retryIfBlockedOn(suspenseInstance);
500 }
501
502 export function clearSuspenseBoundaryFromContainer(
@@ -505,6 +510,8 @@ export function clearSuspenseBoundaryFromContainer(
510 } else {
511 // Document nodes should never contain suspense boundaries.
512 }
513 + // Retry if any event replaying was blocked on this.
514 + retryIfBlockedOn(container);
515 }
516
517 export function hideInstance(instance: Instance): void {
@@ -744,6 +751,18 @@ export function getParentSuspenseInstance(
751 return null;
752 }
753
754 +export function commitHydratedContainer(container: Container): void {
755 + // Retry if any event replaying was blocked on this.
756 + retryIfBlockedOn(container);
757 +}
758 +
759 +export function commitHydratedSuspenseInstance(
760 + suspenseInstance: SuspenseInstance,
761 +): void {
762 + // Retry if any event replaying was blocked on this.
763 + retryIfBlockedOn(suspenseInstance);
764 +}
765 +
766 export function didNotMatchHydratedContainerTextInstance(
767 parentContainer: Container,
768 textInstance: TextInstance,
packages/react-dom/src/events/BeforeInputEventPlugin.js
+1
@@ -464,6 +464,7 @@ const BeforeInputEventPlugin = {
464
465 extractEvents: function(
466 topLevelType,
467 + eventSystemFlags,
468 targetInst,
469 nativeEvent,
470 nativeEventTarget,
packages/react-dom/src/events/ChangeEventPlugin.js
+1
@@ -262,6 +262,7 @@ const ChangeEventPlugin = {
262
263 extractEvents: function(
264 topLevelType,
265 + eventSystemFlags,
266 targetInst,
267 nativeEvent,
268 nativeEventTarget,
packages/react-dom/src/events/EnterLeaveEventPlugin.js
+20 -3
@@ -13,6 +13,7 @@ import {
13 TOP_POINTER_OUT,
14 TOP_POINTER_OVER,
15 } from './DOMTopLevelEventTypes';
16 +import {IS_REPLAYED} from 'legacy-events/EventSystemFlags';
17 import SyntheticMouseEvent from './SyntheticMouseEvent';
18 import SyntheticPointerEvent from './SyntheticPointerEvent';
19 import {
@@ -20,6 +21,7 @@ import {
21 getNodeFromInstance,
22 } from '../client/ReactDOMComponentTree';
23 import {HostComponent, HostText} from 'shared/ReactWorkTags';
24 +import {getNearestMountedFiber} from 'react-reconciler/reflection';
25
26 const eventTypes = {
27 mouseEnter: {
@@ -52,6 +54,7 @@ const EnterLeaveEventPlugin = {
54 */
55 extractEvents: function(
56 topLevelType,
57 + eventSystemFlags,
58 targetInst,
59 nativeEvent,
60 nativeEventTarget,
@@ -61,7 +64,15 @@ const EnterLeaveEventPlugin = {
64 const isOutEvent =
65 topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_POINTER_OUT;
66
64 - if (isOverEvent && (nativeEvent.relatedTarget || nativeEvent.fromElement)) {
67 + if (
68 + isOverEvent &&
69 + (eventSystemFlags & IS_REPLAYED) === 0 &&
70 + (nativeEvent.relatedTarget || nativeEvent.fromElement)
71 + ) {
72 + // If this is an over event with a target, then we've already dispatched
73 + // the event in the out event of the other target. If this is replayed,
74 + // then it's because we couldn't dispatch against this target previously
75 + // so we have to do it now instead.
76 return null;
77 }
78
@@ -90,8 +101,14 @@ const EnterLeaveEventPlugin = {
101 from = targetInst;
102 const related = nativeEvent.relatedTarget || nativeEvent.toElement;
103 to = related ? getClosestInstanceFromNode(related) : null;
93 - if (to !== null && to.tag !== HostComponent && to.tag !== HostText) {
94 - to = null;
104 + if (to !== null) {
105 + const nearestMounted = getNearestMountedFiber(to);
106 + if (
107 + to !== nearestMounted ||
108 + (to.tag !== HostComponent && to.tag !== HostText)
109 + ) {
110 + to = null;
111 + }
112 }
113 } else {
114 // Moving to a node from outside the window.
packages/react-dom/src/events/ReactBrowserEventEmitter.js
+44 -36
@@ -134,43 +134,51 @@ export function listenTo(
134
135 for (let i = 0; i < dependencies.length; i++) {
136 const dependency = dependencies[i];
137 - if (!listeningSet.has(dependency)) {
138 - switch (dependency) {
139 - case TOP_SCROLL:
140 - trapCapturedEvent(TOP_SCROLL, mountAt);
141 - break;
142 - case TOP_FOCUS:
143 - case TOP_BLUR:
144 - trapCapturedEvent(TOP_FOCUS, mountAt);
145 - trapCapturedEvent(TOP_BLUR, mountAt);
146 - // We set the flag for a single dependency later in this function,
147 - // but this ensures we mark both as attached rather than just one.
148 - listeningSet.add(TOP_BLUR);
149 - listeningSet.add(TOP_FOCUS);
150 - break;
151 - case TOP_CANCEL:
152 - case TOP_CLOSE:
153 - if (isEventSupported(getRawEventName(dependency))) {
154 - trapCapturedEvent(dependency, mountAt);
155 - }
156 - break;
157 - case TOP_INVALID:
158 - case TOP_SUBMIT:
159 - case TOP_RESET:
160 - // We listen to them on the target DOM elements.
161 - // Some of them bubble so we don't want them to fire twice.
162 - break;
163 - default:
164 - // By default, listen on the top level to all non-media events.
165 - // Media events don't bubble so adding the listener wouldn't do anything.
166 - const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
167 - if (!isMediaEvent) {
168 - trapBubbledEvent(dependency, mountAt);
169 - }
170 - break;
171 - }
172 - listeningSet.add(dependency);
137 + listenToTopLevel(dependency, mountAt, listeningSet);
138 + }
139 +}
140 +
141 +export function listenToTopLevel(
142 + topLevelType: DOMTopLevelEventType,
143 + mountAt: Document | Element | Node,
144 + listeningSet: Set<DOMTopLevelEventType | string>,
145 +): void {
146 + if (!listeningSet.has(topLevelType)) {
147 + switch (topLevelType) {
148 + case TOP_SCROLL:
149 + trapCapturedEvent(TOP_SCROLL, mountAt);
150 + break;
151 + case TOP_FOCUS:
152 + case TOP_BLUR:
153 + trapCapturedEvent(TOP_FOCUS, mountAt);
154 + trapCapturedEvent(TOP_BLUR, mountAt);
155 + // We set the flag for a single dependency later in this function,
156 + // but this ensures we mark both as attached rather than just one.
157 + listeningSet.add(TOP_BLUR);
158 + listeningSet.add(TOP_FOCUS);
159 + break;
160 + case TOP_CANCEL:
161 + case TOP_CLOSE:
162 + if (isEventSupported(getRawEventName(topLevelType))) {
163 + trapCapturedEvent(topLevelType, mountAt);
164 + }
165 + break;
166 + case TOP_INVALID:
167 + case TOP_SUBMIT:
168 + case TOP_RESET:
169 + // We listen to them on the target DOM elements.
170 + // Some of them bubble so we don't want them to fire twice.
171 + break;
172 + default:
173 + // By default, listen on the top level to all non-media events.
174 + // Media events don't bubble so adding the listener wouldn't do anything.
175 + const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;
176 + if (!isMediaEvent) {
177 + trapBubbledEvent(topLevelType, mountAt);
178 + }
179 + break;
180 }
181 + listeningSet.add(topLevelType);
182 }
183 }
184
packages/react-dom/src/events/ReactDOMEventListener.js
+131 -16
@@ -9,6 +9,8 @@
9
10 import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
11 import type {Fiber} from 'react-reconciler/src/ReactFiber';
12 +import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
13 +import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
14 import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
15
16 // Intentionally not named imports because Rollup would use dynamic dispatch for
@@ -21,8 +23,19 @@ import {
23 flushDiscreteUpdatesIfNeeded,
24 } from 'legacy-events/ReactGenericBatching';
25 import {runExtractedPluginEventsInBatch} from 'legacy-events/EventPluginHub';
24 -import {dispatchEventForResponderEventSystem} from '../events/DOMEventResponderSystem';
25 -import {getNearestMountedFiber} from 'react-reconciler/reflection';
26 +import {dispatchEventForResponderEventSystem} from './DOMEventResponderSystem';
27 +import {
28 + isReplayableDiscreteEvent,
29 + queueDiscreteEvent,
30 + hasQueuedDiscreteEvents,
31 + clearIfContinuousEvent,
32 + queueIfContinuousEvent,
33 +} from './ReactDOMEventReplaying';
34 +import {
35 + getNearestMountedFiber,
36 + getContainerFromFiber,
37 + getSuspenseInstanceFromFiber,
38 +} from 'react-reconciler/reflection';
39 import {
40 HostRoot,
41 SuspenseComponent,
@@ -69,12 +82,13 @@ const {getEventPriority} = SimpleEventPlugin;
82 const CALLBACK_BOOKKEEPING_POOL_SIZE = 10;
83 const callbackBookkeepingPool = [];
84
72 -type BookKeepingInstance = {
85 +type BookKeepingInstance = {|
86 topLevelType: DOMTopLevelEventType | null,
87 + eventSystemFlags: EventSystemFlags,
88 nativeEvent: AnyNativeEvent | null,
89 targetInst: Fiber | null,
90 ancestors: Array<Fiber | null>,
77 -};
91 +|};
92
93 /**
94 * Find the deepest React component completely containing the root of the
@@ -103,16 +117,19 @@ function getTopLevelCallbackBookKeeping(
117 topLevelType: DOMTopLevelEventType,
118 nativeEvent: AnyNativeEvent,
119 targetInst: Fiber | null,
120 + eventSystemFlags: EventSystemFlags,
121 ): BookKeepingInstance {
122 if (callbackBookkeepingPool.length) {
123 const instance = callbackBookkeepingPool.pop();
124 instance.topLevelType = topLevelType;
125 + instance.eventSystemFlags = eventSystemFlags;
126 instance.nativeEvent = nativeEvent;
127 instance.targetInst = targetInst;
128 return instance;
129 }
130 return {
131 topLevelType,
132 + eventSystemFlags,
133 nativeEvent,
134 targetInst,
135 ancestors: [],
@@ -164,6 +181,7 @@ function handleTopLevel(bookKeeping: BookKeepingInstance) {
181
182 runExtractedPluginEventsInBatch(
183 topLevelType,
184 + bookKeeping.eventSystemFlags,
185 targetInst,
186 nativeEvent,
187 eventTarget,
@@ -300,6 +318,7 @@ function dispatchEventForPluginEventSystem(
318 topLevelType,
319 nativeEvent,
320 targetInst,
321 + eventSystemFlags,
322 );
323
324 try {
@@ -319,28 +338,121 @@ export function dispatchEvent(
338 if (!_enabled) {
339 return;
340 }
341 + if (hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(topLevelType)) {
342 + // If we already have a queue of discrete events, and this is another discrete
343 + // event, then we can't dispatch it regardless of its target, since they
344 + // need to dispatch in order.
345 + queueDiscreteEvent(
346 + null, // Flags that we're not actually blocked on anything as far as we know.
347 + topLevelType,
348 + eventSystemFlags,
349 + nativeEvent,
350 + );
351 + return;
352 + }
353 +
354 + const blockedOn = attemptToDispatchEvent(
355 + topLevelType,
356 + eventSystemFlags,
357 + nativeEvent,
358 + );
359 +
360 + if (blockedOn === null) {
361 + // We successfully dispatched this event.
362 + clearIfContinuousEvent(topLevelType, nativeEvent);
363 + return;
364 + }
365 +
366 + if (isReplayableDiscreteEvent(topLevelType)) {
367 + // This this to be replayed later once the target is available.
368 + queueDiscreteEvent(blockedOn, topLevelType, eventSystemFlags, nativeEvent);
369 + return;
370 + }
371 +
372 + if (
373 + queueIfContinuousEvent(
374 + blockedOn,
375 + topLevelType,
376 + eventSystemFlags,
377 + nativeEvent,
378 + )
379 + ) {
380 + return;
381 + }
382 +
383 + // We need to clear only if we didn't queue because
384 + // queueing is accummulative.
385 + clearIfContinuousEvent(topLevelType, nativeEvent);
386 +
387 + // This is not replayable so we'll invoke it but without a target,
388 + // in case the event system needs to trace it.
389 + if (enableFlareAPI) {
390 + if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) {
391 + dispatchEventForPluginEventSystem(
392 + topLevelType,
393 + eventSystemFlags,
394 + nativeEvent,
395 + null,
396 + );
397 + }
398 + if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) {
399 + // React Flare event system
400 + dispatchEventForResponderEventSystem(
401 + (topLevelType: any),
402 + null,
403 + nativeEvent,
404 + getEventTarget(nativeEvent),
405 + eventSystemFlags,
406 + );
407 + }
408 + } else {
409 + dispatchEventForPluginEventSystem(
410 + topLevelType,
411 + eventSystemFlags,
412 + nativeEvent,
413 + null,
414 + );
415 + }
416 +}
417 +
418 +// Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked.
419 +export function attemptToDispatchEvent(
420 + topLevelType: DOMTopLevelEventType,
421 + eventSystemFlags: EventSystemFlags,
422 + nativeEvent: AnyNativeEvent,
423 +): null | Container | SuspenseInstance {
424 + // TODO: Warn if _enabled is false.
425 +
426 const nativeEventTarget = getEventTarget(nativeEvent);
427 let targetInst = getClosestInstanceFromNode(nativeEventTarget);
428
429 if (targetInst !== null) {
430 let nearestMounted = getNearestMountedFiber(targetInst);
431 if (nearestMounted === null) {
328 - // This tree has been unmounted already.
432 + // This tree has been unmounted already. Dispatch without a target.
433 targetInst = null;
434 } else {
435 const tag = nearestMounted.tag;
436 if (tag === SuspenseComponent) {
333 - // TODO: This is a good opportunity to schedule a replay of
334 - // the event instead once this boundary has been hydrated.
335 - // For now we're going to just ignore this event as if it's
336 - // not mounted.
437 + let instance = getSuspenseInstanceFromFiber(nearestMounted);
438 + if (instance !== null) {
439 + // Queue the event to be replayed later. Abort dispatching since we
440 + // don't want this event dispatched twice through the event system.
441 + // TODO: If this is the first discrete event in the queue. Schedule an increased
442 + // priority for this boundary.
443 + return instance;
444 + }
445 + // This shouldn't happen, something went wrong but to avoid blocking
446 + // the whole system, dispatch the event without a target.
447 + // TODO: Warn.
448 targetInst = null;
449 } else if (tag === HostRoot) {
339 - // We have not yet mounted/hydrated the first children.
340 - // TODO: This is a good opportunity to schedule a replay of
341 - // the event instead once this root has been hydrated.
342 - // For now we're going to just ignore this event as if it's
343 - // not mounted.
450 + const root: FiberRoot = nearestMounted.stateNode;
451 + if (root.hydrate) {
452 + // If this happens during a replay something went wrong and it might block
453 + // the whole system.
454 + return getContainerFromFiber(nearestMounted);
455 + }
456 targetInst = null;
457 } else if (nearestMounted !== targetInst) {
458 // If we get an event (ex: img onload) before committing that
@@ -353,14 +465,15 @@ export function dispatchEvent(
465 }
466
467 if (enableFlareAPI) {
356 - if (eventSystemFlags === PLUGIN_EVENT_SYSTEM) {
468 + if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) {
469 dispatchEventForPluginEventSystem(
470 topLevelType,
471 eventSystemFlags,
472 nativeEvent,
473 targetInst,
474 );
363 - } else {
475 + }
476 + if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) {
477 // React Flare event system
478 dispatchEventForResponderEventSystem(
479 (topLevelType: any),
@@ -378,4 +491,6 @@ export function dispatchEvent(
491 targetInst,
492 );
493 }
494 + // We're not blocked on anything.
495 + return null;
496 }
packages/react-dom/src/events/ReactDOMEventReplaying.js new
+487
@@ -0,0 +1,487 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
11 +import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
12 +import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
13 +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
14 +
15 +import {enableFlareAPI} from 'shared/ReactFeatureFlags';
16 +import {
17 + unstable_scheduleCallback as scheduleCallback,
18 + unstable_NormalPriority as NormalPriority,
19 +} from 'scheduler';
20 +import {
21 + attemptToDispatchEvent,
22 + trapEventForResponderEventSystem,
23 +} from './ReactDOMEventListener';
24 +import {
25 + getListeningSetForElement,
26 + listenToTopLevel,
27 +} from './ReactBrowserEventEmitter';
28 +import {unsafeCastDOMTopLevelTypeToString} from 'legacy-events/TopLevelEventTypes';
29 +
30 +// TODO: Upgrade this definition once we're on a newer version of Flow that
31 +// has this definition built-in.
32 +type PointerEvent = Event & {
33 + pointerId: number,
34 + relatedTarget: EventTarget | null,
35 +};
36 +
37 +import {
38 + TOP_MOUSE_DOWN,
39 + TOP_MOUSE_UP,
40 + TOP_TOUCH_CANCEL,
41 + TOP_TOUCH_END,
42 + TOP_TOUCH_START,
43 + TOP_AUX_CLICK,
44 + TOP_DOUBLE_CLICK,
45 + TOP_POINTER_CANCEL,
46 + TOP_POINTER_DOWN,
47 + TOP_POINTER_UP,
48 + TOP_DRAG_END,
49 + TOP_DRAG_START,
50 + TOP_DROP,
51 + TOP_COMPOSITION_END,
52 + TOP_COMPOSITION_START,
53 + TOP_KEY_DOWN,
54 + TOP_KEY_PRESS,
55 + TOP_KEY_UP,
56 + TOP_INPUT,
57 + TOP_TEXT_INPUT,
58 + TOP_CLOSE,
59 + TOP_CANCEL,
60 + TOP_COPY,
61 + TOP_CUT,
62 + TOP_PASTE,
63 + TOP_CLICK,
64 + TOP_CHANGE,
65 + TOP_CONTEXT_MENU,
66 + TOP_RESET,
67 + TOP_SUBMIT,
68 + TOP_DRAG_ENTER,
69 + TOP_DRAG_LEAVE,
70 + TOP_MOUSE_OVER,
71 + TOP_MOUSE_OUT,
72 + TOP_POINTER_OVER,
73 + TOP_POINTER_OUT,
74 + TOP_GOT_POINTER_CAPTURE,
75 + TOP_LOST_POINTER_CAPTURE,
76 + TOP_FOCUS,
77 + TOP_BLUR,
78 +} from './DOMTopLevelEventTypes';
79 +import {IS_REPLAYED} from 'legacy-events/EventSystemFlags';
80 +
81 +type QueuedReplayableEvent = {|
82 + blockedOn: null | Container | SuspenseInstance,
83 + topLevelType: DOMTopLevelEventType,
84 + eventSystemFlags: EventSystemFlags,
85 + nativeEvent: AnyNativeEvent,
86 +|};
87 +
88 +let hasScheduledReplayAttempt = false;
89 +
90 +// The queue of discrete events to be replayed.
91 +let queuedDiscreteEvents: Array<QueuedReplayableEvent> = [];
92 +
93 +// Indicates if any continuous event targets are non-null for early bailout.
94 +let hasAnyQueuedContinuousEvents: boolean = false;
95 +// The last of each continuous event type. We only need to replay the last one
96 +// if the last target was dehydrated.
97 +let queuedFocus: null | QueuedReplayableEvent = null;
98 +let queuedDrag: null | QueuedReplayableEvent = null;
99 +let queuedMouse: null | QueuedReplayableEvent = null;
100 +// For pointer events there can be one latest event per pointerId.
101 +let queuedPointers: Map<number, QueuedReplayableEvent> = new Map();
102 +let queuedPointerCaptures: Map<number, QueuedReplayableEvent> = new Map();
103 +// We could consider replaying selectionchange and touchmoves too.
104 +
105 +export function hasQueuedDiscreteEvents(): boolean {
106 + return queuedDiscreteEvents.length > 0;
107 +}
108 +
109 +export function hasQueuedContinuousEvents(): boolean {
110 + return hasAnyQueuedContinuousEvents;
111 +}
112 +
113 +const discreteReplayableEvents = [
114 + TOP_MOUSE_DOWN,
115 + TOP_MOUSE_UP,
116 + TOP_TOUCH_CANCEL,
117 + TOP_TOUCH_END,
118 + TOP_TOUCH_START,
119 + TOP_AUX_CLICK,
120 + TOP_DOUBLE_CLICK,
121 + TOP_POINTER_CANCEL,
122 + TOP_POINTER_DOWN,
123 + TOP_POINTER_UP,
124 + TOP_DRAG_END,
125 + TOP_DRAG_START,
126 + TOP_DROP,
127 + TOP_COMPOSITION_END,
128 + TOP_COMPOSITION_START,
129 + TOP_KEY_DOWN,
130 + TOP_KEY_PRESS,
131 + TOP_KEY_UP,
132 + TOP_INPUT,
133 + TOP_TEXT_INPUT,
134 + TOP_CLOSE,
135 + TOP_CANCEL,
136 + TOP_COPY,
137 + TOP_CUT,
138 + TOP_PASTE,
139 + TOP_CLICK,
140 + TOP_CHANGE,
141 + TOP_CONTEXT_MENU,
142 + TOP_RESET,
143 + TOP_SUBMIT,
144 +];
145 +
146 +const continuousReplayableEvents = [
147 + TOP_FOCUS,
148 + TOP_BLUR,
149 + TOP_DRAG_ENTER,
150 + TOP_DRAG_LEAVE,
151 + TOP_MOUSE_OVER,
152 + TOP_MOUSE_OUT,
153 + TOP_POINTER_OVER,
154 + TOP_POINTER_OUT,
155 + TOP_GOT_POINTER_CAPTURE,
156 + TOP_LOST_POINTER_CAPTURE,
157 +];
158 +
159 +export function isReplayableDiscreteEvent(
160 + eventType: DOMTopLevelEventType,
161 +): boolean {
162 + return discreteReplayableEvents.indexOf(eventType) > -1;
163 +}
164 +
165 +function trapReplayableEvent(
166 + topLevelType: DOMTopLevelEventType,
167 + document: Document,
168 + listeningSet: Set<DOMTopLevelEventType | string>,
169 +) {
170 + listenToTopLevel(topLevelType, document, listeningSet);
171 + if (enableFlareAPI) {
172 + // Trap events for the responder system.
173 + const passiveEventKey =
174 + unsafeCastDOMTopLevelTypeToString(topLevelType) + '_passive';
175 + if (!listeningSet.has(passiveEventKey)) {
176 + trapEventForResponderEventSystem(document, topLevelType, true);
177 + listeningSet.add(passiveEventKey);
178 + }
179 + // TODO: This listens to all events as active which might have
180 + // undesirable effects. It's also unnecessary to have both
181 + // passive and active listeners. Instead, we could start with
182 + // a passive and upgrade it to an active one if needed.
183 + // For replaying purposes the active is never needed since we
184 + // currently don't preventDefault.
185 + const activeEventKey =
186 + unsafeCastDOMTopLevelTypeToString(topLevelType) + '_active';
187 + if (!listeningSet.has(activeEventKey)) {
188 + trapEventForResponderEventSystem(document, topLevelType, false);
189 + listeningSet.add(activeEventKey);
190 + }
191 + }
192 +}
193 +
194 +export function eagerlyTrapReplayableEvents(document: Document) {
195 + const listeningSet = getListeningSetForElement(document);
196 + // Discrete
197 + discreteReplayableEvents.forEach(topLevelType => {
198 + trapReplayableEvent(topLevelType, document, listeningSet);
199 + });
200 + // Continuous
201 + continuousReplayableEvents.forEach(topLevelType => {
202 + trapReplayableEvent(topLevelType, document, listeningSet);
203 + });
204 +}
205 +
206 +function createQueuedReplayableEvent(
207 + blockedOn: null | Container | SuspenseInstance,
208 + topLevelType: DOMTopLevelEventType,
209 + eventSystemFlags: EventSystemFlags,
210 + nativeEvent: AnyNativeEvent,
211 +): QueuedReplayableEvent {
212 + return {
213 + blockedOn,
214 + topLevelType,
215 + eventSystemFlags: eventSystemFlags | IS_REPLAYED,
216 + nativeEvent,
217 + };
218 +}
219 +
220 +export function queueDiscreteEvent(
221 + blockedOn: null | Container | SuspenseInstance,
222 + topLevelType: DOMTopLevelEventType,
223 + eventSystemFlags: EventSystemFlags,
224 + nativeEvent: AnyNativeEvent,
225 +): void {
226 + queuedDiscreteEvents.push(
227 + createQueuedReplayableEvent(
228 + blockedOn,
229 + topLevelType,
230 + eventSystemFlags,
231 + nativeEvent,
232 + ),
233 + );
234 + if (blockedOn === null && queuedDiscreteEvents.length === 1) {
235 + // This probably shouldn't happen but some defensive coding might
236 + // help us get unblocked if we have a bug.
237 + replayUnblockedEvents();
238 + }
239 +}
240 +
241 +// Resets the replaying for this type of continuous event to no event.
242 +export function clearIfContinuousEvent(
243 + topLevelType: DOMTopLevelEventType,
244 + nativeEvent: AnyNativeEvent,
245 +): void {
246 + switch (topLevelType) {
247 + case TOP_FOCUS:
248 + case TOP_BLUR:
249 + queuedFocus = null;
250 + break;
251 + case TOP_DRAG_ENTER:
252 + case TOP_DRAG_LEAVE:
253 + queuedDrag = null;
254 + break;
255 + case TOP_MOUSE_OVER:
256 + case TOP_MOUSE_OUT:
257 + queuedMouse = null;
258 + break;
259 + case TOP_POINTER_OVER:
260 + case TOP_POINTER_OUT: {
261 + let pointerId = ((nativeEvent: any): PointerEvent).pointerId;
262 + queuedPointers.delete(pointerId);
263 + break;
264 + }
265 + case TOP_GOT_POINTER_CAPTURE:
266 + case TOP_LOST_POINTER_CAPTURE: {
267 + let pointerId = ((nativeEvent: any): PointerEvent).pointerId;
268 + queuedPointerCaptures.delete(pointerId);
269 + break;
270 + }
271 + }
272 +}
273 +
274 +function accumulateOrCreateQueuedReplayableEvent(
275 + existingQueuedEvent: null | QueuedReplayableEvent,
276 + blockedOn: null | Container | SuspenseInstance,
277 + topLevelType: DOMTopLevelEventType,
278 + eventSystemFlags: EventSystemFlags,
279 + nativeEvent: AnyNativeEvent,
280 +): QueuedReplayableEvent {
281 + if (
282 + existingQueuedEvent === null ||
283 + existingQueuedEvent.nativeEvent !== nativeEvent
284 + ) {
285 + return createQueuedReplayableEvent(
286 + blockedOn,
287 + topLevelType,
288 + eventSystemFlags,
289 + nativeEvent,
290 + );
291 + }
292 + // If we have already queued this exact event, then it's because
293 + // the different event systems have different DOM event listeners.
294 + // We can accumulate the flags and store a single event to be
295 + // replayed.
296 + existingQueuedEvent.eventSystemFlags |= eventSystemFlags;
297 + return existingQueuedEvent;
298 +}
299 +
300 +export function queueIfContinuousEvent(
301 + blockedOn: null | Container | SuspenseInstance,
302 + topLevelType: DOMTopLevelEventType,
303 + eventSystemFlags: EventSystemFlags,
304 + nativeEvent: AnyNativeEvent,
305 +): boolean {
306 + // These set relatedTarget to null because the replayed event will be treated as if we
307 + // moved from outside the window (no target) onto the target once it hydrates.
308 + // Instead of mutating we could clone the event.
309 + switch (topLevelType) {
310 + case TOP_FOCUS: {
311 + const focusEvent = ((nativeEvent: any): FocusEvent);
312 + queuedFocus = accumulateOrCreateQueuedReplayableEvent(
313 + queuedFocus,
314 + blockedOn,
315 + topLevelType,
316 + eventSystemFlags,
317 + focusEvent,
318 + );
319 + return true;
320 + }
321 + case TOP_DRAG_ENTER: {
322 + const dragEvent = ((nativeEvent: any): DragEvent);
323 + queuedDrag = accumulateOrCreateQueuedReplayableEvent(
324 + queuedDrag,
325 + blockedOn,
326 + topLevelType,
327 + eventSystemFlags,
328 + dragEvent,
329 + );
330 + return true;
331 + }
332 + case TOP_MOUSE_OVER: {
333 + const mouseEvent = ((nativeEvent: any): MouseEvent);
334 + queuedMouse = accumulateOrCreateQueuedReplayableEvent(
335 + queuedMouse,
336 + blockedOn,
337 + topLevelType,
338 + eventSystemFlags,
339 + mouseEvent,
340 + );
341 + return true;
342 + }
343 + case TOP_POINTER_OVER: {
344 + const pointerEvent = ((nativeEvent: any): PointerEvent);
345 + const pointerId = pointerEvent.pointerId;
346 + queuedPointers.set(
347 + pointerId,
348 + accumulateOrCreateQueuedReplayableEvent(
349 + queuedPointers.get(pointerId) || null,
350 + blockedOn,
351 + topLevelType,
352 + eventSystemFlags,
353 + pointerEvent,
354 + ),
355 + );
356 + return true;
357 + }
358 + case TOP_GOT_POINTER_CAPTURE: {
359 + const pointerEvent = ((nativeEvent: any): PointerEvent);
360 + const pointerId = pointerEvent.pointerId;
361 + queuedPointerCaptures.set(
362 + pointerId,
363 + accumulateOrCreateQueuedReplayableEvent(
364 + queuedPointerCaptures.get(pointerId) || null,
365 + blockedOn,
366 + topLevelType,
367 + eventSystemFlags,
368 + pointerEvent,
369 + ),
370 + );
371 + return true;
372 + }
373 + }
374 + return false;
375 +}
376 +
377 +function attemptReplayQueuedEvent(queuedEvent: QueuedReplayableEvent): boolean {
378 + if (queuedEvent.blockedOn !== null) {
379 + return false;
380 + }
381 + let nextBlockedOn = attemptToDispatchEvent(
382 + queuedEvent.topLevelType,
383 + queuedEvent.eventSystemFlags,
384 + queuedEvent.nativeEvent,
385 + );
386 + if (nextBlockedOn !== null) {
387 + // We're still blocked. Try again later.
388 + queuedEvent.blockedOn = nextBlockedOn;
389 + return false;
390 + }
391 + return true;
392 +}
393 +
394 +function attemptReplayQueuedEventInMap(
395 + queuedEvent: QueuedReplayableEvent,
396 + key: number,
397 + map: Map<number, QueuedReplayableEvent>,
398 +): void {
399 + if (attemptReplayQueuedEvent(queuedEvent)) {
400 + map.delete(key);
401 + }
402 +}
403 +
404 +function replayUnblockedEvents() {
405 + hasScheduledReplayAttempt = false;
406 + // First replay discrete events.
407 + while (queuedDiscreteEvents.length > 0) {
408 + let nextDiscreteEvent = queuedDiscreteEvents[0];
409 + if (nextDiscreteEvent.blockedOn !== null) {
410 + // We're still blocked.
411 + break;
412 + }
413 + let nextBlockedOn = attemptToDispatchEvent(
414 + nextDiscreteEvent.topLevelType,
415 + nextDiscreteEvent.eventSystemFlags,
416 + nextDiscreteEvent.nativeEvent,
417 + );
418 + if (nextBlockedOn !== null) {
419 + // We're still blocked. Try again later.
420 + nextDiscreteEvent.blockedOn = nextBlockedOn;
421 + } else {
422 + // We've successfully replayed the first event. Let's try the next one.
423 + queuedDiscreteEvents.shift();
424 + }
425 + }
426 + // Next replay any continuous events.
427 + if (queuedFocus !== null && attemptReplayQueuedEvent(queuedFocus)) {
428 + queuedFocus = null;
429 + }
430 + if (queuedDrag !== null && attemptReplayQueuedEvent(queuedDrag)) {
431 + queuedDrag = null;
432 + }
433 + if (queuedMouse !== null && attemptReplayQueuedEvent(queuedMouse)) {
434 + queuedMouse = null;
435 + }
436 + queuedPointers.forEach(attemptReplayQueuedEventInMap);
437 + queuedPointerCaptures.forEach(attemptReplayQueuedEventInMap);
438 +}
439 +
440 +function scheduleCallbackIfUnblocked(
441 + queuedEvent: QueuedReplayableEvent,
442 + unblocked: Container | SuspenseInstance,
443 +) {
444 + if (queuedEvent.blockedOn === unblocked) {
445 + queuedEvent.blockedOn = null;
446 + if (!hasScheduledReplayAttempt) {
447 + hasScheduledReplayAttempt = true;
448 + // Schedule a callback to attempt replaying as many events as are
449 + // now unblocked. This first might not actually be unblocked yet.
450 + // We could check it early to avoid scheduling an unnecessary callback.
451 + scheduleCallback(NormalPriority, replayUnblockedEvents);
452 + }
453 + }
454 +}
455 +
456 +export function retryIfBlockedOn(
457 + unblocked: Container | SuspenseInstance,
458 +): void {
459 + // Mark anything that was blocked on this as no longer blocked
460 + // and eligible for a replay.
461 + if (queuedDiscreteEvents.length > 0) {
462 + scheduleCallbackIfUnblocked(queuedDiscreteEvents[0], unblocked);
463 + // This is a exponential search for each boundary that commits. I think it's
464 + // worth it because we expect very few discrete events to queue up and once
465 + // we are actually fully unblocked it will be fast to replay them.
466 + for (let i = 1; i < queuedDiscreteEvents.length; i++) {
467 + let queuedEvent = queuedDiscreteEvents[i];
468 + if (queuedEvent.blockedOn === unblocked) {
469 + queuedEvent.blockedOn = null;
470 + }
471 + }
472 + }
473 +
474 + if (queuedFocus !== null) {
475 + scheduleCallbackIfUnblocked(queuedFocus, unblocked);
476 + }
477 + if (queuedDrag !== null) {
478 + scheduleCallbackIfUnblocked(queuedDrag, unblocked);
479 + }
480 + if (queuedMouse !== null) {
481 + scheduleCallbackIfUnblocked(queuedMouse, unblocked);
482 + }
483 + const unblock = queuedEvent =>
484 + scheduleCallbackIfUnblocked(queuedEvent, unblocked);
485 + queuedPointers.forEach(unblock);
486 + queuedPointerCaptures.forEach(unblock);
487 +}
packages/react-dom/src/events/SelectEventPlugin.js
+1
@@ -162,6 +162,7 @@ const SelectEventPlugin = {
162
163 extractEvents: function(
164 topLevelType,
165 + eventSystemFlags,
166 targetInst,
167 nativeEvent,
168 nativeEventTarget,
packages/react-dom/src/events/SimpleEventPlugin.js
+2
@@ -18,6 +18,7 @@ import type {
18 } from 'legacy-events/ReactSyntheticEventType';
19 import type {Fiber} from 'react-reconciler/src/ReactFiber';
20 import type {EventTypes, PluginModule} from 'legacy-events/PluginModuleType';
21 +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
22
23 import {
24 DiscreteEvent,
@@ -247,6 +248,7 @@ const SimpleEventPlugin: PluginModule<MouseEvent> & {
248
249 extractEvents: function(
250 topLevelType: TopLevelType,
251 + eventSystemFlags: EventSystemFlags,
252 targetInst: null | Fiber,
253 nativeEvent: MouseEvent,
254 nativeEventTarget: EventTarget,
packages/react-native-renderer/src/ReactFabricEventEmitter.js
+2
@@ -9,6 +9,7 @@
9
10 import type {Fiber} from 'react-reconciler/src/ReactFiber';
11
12 +import {PLUGIN_EVENT_SYSTEM} from 'legacy-events/EventSystemFlags';
13 import {
14 getListener,
15 runExtractedPluginEventsInBatch,
@@ -41,6 +42,7 @@ export function dispatchEvent(
42 // Heritage plugin event system
43 runExtractedPluginEventsInBatch(
44 topLevelType,
45 + PLUGIN_EVENT_SYSTEM,
46 targetFiber,
47 nativeEvent,
48 nativeEvent.target,
packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js
+2
@@ -8,6 +8,7 @@
8 */
9
10 import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
11 +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
12 import {
13 accumulateTwoPhaseDispatches,
14 accumulateDirectDispatches,
@@ -32,6 +33,7 @@ const ReactNativeBridgeEventPlugin = {
33 */
34 extractEvents: function(
35 topLevelType: TopLevelType,
36 + eventSystemFlags: EventSystemFlags,
37 targetInst: null | Object,
38 nativeEvent: AnyNativeEvent,
39 nativeEventTarget: Object,
packages/react-native-renderer/src/ReactNativeEventEmitter.js
+2
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import {PLUGIN_EVENT_SYSTEM} from 'legacy-events/EventSystemFlags';
11 import {
12 getListener,
13 runExtractedPluginEventsInBatch,
@@ -100,6 +101,7 @@ function _receiveRootNodeIDEvent(
101 batchedUpdates(function() {
102 runExtractedPluginEventsInBatch(
103 topLevelType,
104 + PLUGIN_EVENT_SYSTEM,
105 inst,
106 nativeEvent,
107 nativeEvent.target,
packages/react-reconciler/src/ReactFiberCommitWork.js
+26 -17
@@ -103,6 +103,8 @@ import {
103 unmountResponderInstance,
104 unmountFundamentalComponent,
105 updateFundamentalComponent,
106 + commitHydratedContainer,
107 + commitHydratedSuspenseInstance,
108 } from './ReactFiberHostConfig';
109 import {
110 captureCommitPhaseError,
@@ -612,9 +614,7 @@ function commitLifeCycles(
614 return;
615 }
616 case SuspenseComponent: {
615 - if (enableSuspenseCallback) {
616 - commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
617 - }
617 + commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
618 return;
619 }
620 case SuspenseListComponent:
@@ -1306,11 +1306,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1306 return;
1307 }
1308 case HostRoot: {
1309 - const root: FiberRoot = finishedWork.stateNode;
1309 if (supportsHydration) {
1310 + const root: FiberRoot = finishedWork.stateNode;
1311 if (root.hydrate) {
1312 // We've just hydrated. No need to hydrate again.
1313 root.hydrate = false;
1314 + commitHydratedContainer(root.containerInfo);
1315 }
1316 }
1317 break;
@@ -1384,11 +1385,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1385 return;
1386 }
1387 case HostRoot: {
1387 - const root: FiberRoot = finishedWork.stateNode;
1388 if (supportsHydration) {
1389 + const root: FiberRoot = finishedWork.stateNode;
1390 if (root.hydrate) {
1391 // We've just hydrated. No need to hydrate again.
1392 root.hydrate = false;
1393 + commitHydratedContainer(root.containerInfo);
1394 }
1395 }
1396 return;
@@ -1477,18 +1479,25 @@ function commitSuspenseHydrationCallbacks(
1479 finishedRoot: FiberRoot,
1480 finishedWork: Fiber,
1481 ) {
1480 - if (enableSuspenseCallback) {
1481 - const hydrationCallbacks = finishedRoot.hydrationCallbacks;
1482 - if (hydrationCallbacks !== null) {
1483 - const onHydrated = hydrationCallbacks.onHydrated;
1484 - if (onHydrated) {
1485 - const newState: SuspenseState | null = finishedWork.memoizedState;
1486 - if (newState === null) {
1487 - const current = finishedWork.alternate;
1488 - if (current !== null) {
1489 - const prevState: SuspenseState | null = current.memoizedState;
1490 - if (prevState !== null && prevState.dehydrated !== null) {
1491 - onHydrated(prevState.dehydrated);
1482 + if (!supportsHydration) {
1483 + return;
1484 + }
1485 + const newState: SuspenseState | null = finishedWork.memoizedState;
1486 + if (newState === null) {
1487 + const current = finishedWork.alternate;
1488 + if (current !== null) {
1489 + const prevState: SuspenseState | null = current.memoizedState;
1490 + if (prevState !== null) {
1491 + const suspenseInstance = prevState.dehydrated;
1492 + if (suspenseInstance !== null) {
1493 + commitHydratedSuspenseInstance(suspenseInstance);
1494 + if (enableSuspenseCallback) {
1495 + const hydrationCallbacks = finishedRoot.hydrationCallbacks;
1496 + if (hydrationCallbacks !== null) {
1497 + const onHydrated = hydrationCallbacks.onHydrated;
1498 + if (onHydrated) {
1499 + onHydrated(suspenseInstance);
1500 + }
1501 }
1502 }
1503 }
packages/react-reconciler/src/ReactFiberTreeReflection.js
+27
@@ -8,6 +8,8 @@
8 */
9
10 import type {Fiber} from './ReactFiber';
11 +import type {Container, SuspenseInstance} from './ReactFiberHostConfig';
12 +import type {SuspenseState} from './ReactFiberSuspenseComponent';
13
14 import invariant from 'shared/invariant';
15 import warningWithoutStack from 'shared/warningWithoutStack';
@@ -22,6 +24,7 @@ import {
24 HostPortal,
25 HostText,
26 FundamentalComponent,
27 + SuspenseComponent,
28 } from 'shared/ReactWorkTags';
29 import {NoEffect, Placement, Hydrating} from 'shared/ReactSideEffectTags';
30 import {enableFundamentalAPI} from 'shared/ReactFeatureFlags';
@@ -60,6 +63,30 @@ export function getNearestMountedFiber(fiber: Fiber): null | Fiber {
63 return null;
64 }
65
66 +export function getSuspenseInstanceFromFiber(
67 + fiber: Fiber,
68 +): null | SuspenseInstance {
69 + if (fiber.tag === SuspenseComponent) {
70 + let suspenseState: SuspenseState | null = fiber.memoizedState;
71 + if (suspenseState === null) {
72 + const current = fiber.alternate;
73 + if (current !== null) {
74 + suspenseState = current.memoizedState;
75 + }
76 + }
77 + if (suspenseState !== null) {
78 + return suspenseState.dehydrated;
79 + }
80 + }
81 + return null;
82 +}
83 +
84 +export function getContainerFromFiber(fiber: Fiber): null | Container {
85 + return fiber.tag === HostRoot
86 + ? (fiber.stateNode.containerInfo: Container)
87 + : null;
88 +}
89 +
90 export function isFiberMounted(fiber: Fiber): boolean {
91 return getNearestMountedFiber(fiber) === fiber;
92 }
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+3
@@ -129,6 +129,9 @@ export const hydrateTextInstance = $$$hostConfig.hydrateTextInstance;
129 export const hydrateSuspenseInstance = $$$hostConfig.hydrateSuspenseInstance;
130 export const getNextHydratableInstanceAfterSuspenseInstance =
131 $$$hostConfig.getNextHydratableInstanceAfterSuspenseInstance;
132 +export const commitHydratedContainer = $$$hostConfig.commitHydratedContainer;
133 +export const commitHydratedSuspenseInstance =
134 + $$$hostConfig.commitHydratedSuspenseInstance;
135 export const clearSuspenseBoundary = $$$hostConfig.clearSuspenseBoundary;
136 export const clearSuspenseBoundaryFromContainer =
137 $$$hostConfig.clearSuspenseBoundaryFromContainer;
packages/shared/HostConfigWithNoHydration.js
+2
@@ -36,6 +36,8 @@ export const hydrateInstance = shim;
36 export const hydrateTextInstance = shim;
37 export const hydrateSuspenseInstance = shim;
38 export const getNextHydratableInstanceAfterSuspenseInstance = shim;
39 +export const commitHydratedContainer = shim;
40 +export const commitHydratedSuspenseInstance = shim;
41 export const clearSuspenseBoundary = shim;
42 export const clearSuspenseBoundaryFromContainer = shim;
43 export const didNotMatchHydratedContainerTextInstance = shim;