@samitouri / QOS-React / commits / c804f9aebb

ReactDOM.useEvent: wire to event system to the hook (#18304)

Dominic Gannaway committed Mar 16, 2020 at 21:46 UTC c804f9aebb3c951ec6148bd7004e6a6683d53b09
12 files changed +710 -6
packages/legacy-events/EventPluginUtils.js
+9 -1
@@ -79,12 +79,20 @@ export function executeDispatchesInOrder(event) {
79 validateEventDispatches(event);
80 }
81 if (Array.isArray(dispatchListeners)) {
82 + let previousInstance;
83 for (let i = 0; i < dispatchListeners.length; i++) {
83 - if (event.isPropagationStopped()) {
84 + const instance = dispatchInstances[i];
85 + // We check if the instance was the same as the last one,
86 + // if it was, then we're still on the same instance thus
87 + // propagation should not stop. If we add support for
88 + // stopImmediatePropagation at some point, then we'll
89 + // need to handle that case here differently.
90 + if (instance !== previousInstance && event.isPropagationStopped()) {
91 break;
92 }
93 // Listeners and Instances are two parallel arrays that are always in sync.
94 executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
95 + previousInstance = instance;
96 }
97 } else if (dispatchListeners) {
98 executeDispatch(event, dispatchListeners, dispatchInstances);
packages/react-art/src/ReactARTHostConfig.js
+4
@@ -470,6 +470,10 @@ export function beforeRemoveInstance(instance) {
470 // noop
471 }
472
473 +export function registerEvent(event: any, rootContainerInstance: any) {
474 + throw new Error('Not yet implemented.');
475 +}
476 +
477 export function mountEventListener(listener: any) {
478 throw new Error('Not yet implemented.');
479 }
packages/react-dom/src/client/ReactDOMHostConfig.js
+37
@@ -7,12 +7,14 @@
7 * @flow
8 */
9
10 +import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
11 import type {RootType} from './ReactDOMRoot';
12
13 import {
14 precacheFiberNode,
15 updateFiberProps,
16 getClosestInstanceFromNode,
17 + getListenersFromTarget,
18 } from './ReactDOMComponentTree';
19 import {
20 createElement,
@@ -78,7 +80,9 @@ import {
80 detachElementListener,
81 isDOMDocument,
82 isDOMElement,
83 + listenToTopLevelEvent,
84 } from '../events/DOMModernPluginEventSystem';
85 +import {getListenerMapForElement} from '../events/DOMEventListenerMap';
86
87 export type ReactListenerEvent = ReactDOMListenerEvent;
88 export type ReactListenerMap = ReactDOMListenerMap;
@@ -529,6 +533,22 @@ export function beforeRemoveInstance(
533 ) {
534 dispatchBeforeDetachedBlur(((instance: any): HTMLElement));
535 }
536 + if (enableUseEventAPI) {
537 + // It's unfortunate that we have to do this cleanup, but
538 + // it's necessary otherwise we will leak the host instances
539 + // from the useEvent hook instances Map. We call destroy
540 + // on each listener to ensure we properly remove the instance
541 + // from the instances Map. Note: we have this Map so that we
542 + // can properly unmount instances when the function component
543 + // that the hook is attached to gets unmounted.
544 + const listenersSet = getListenersFromTarget(instance);
545 + if (listenersSet !== null) {
546 + const listeners = Array.from(listenersSet);
547 + for (let i = 0; i < listeners.length; i++) {
548 + listeners[i].destroy(instance);
549 + }
550 + }
551 + }
552 }
553
554 export function removeChild(
@@ -1083,6 +1103,23 @@ export function getInstanceFromNode(node: HTMLElement): null | Object {
1103 return getClosestInstanceFromNode(node) || null;
1104 }
1105
1106 +export function registerEvent(
1107 + event: ReactDOMListenerEvent,
1108 + rootContainerInstance: Container,
1109 +): void {
1110 + const {passive, priority, type} = event;
1111 + const listenerMap = getListenerMapForElement(rootContainerInstance);
1112 + // Add the event listener to the target container (falling back to
1113 + // the target if we didn't find one).
1114 + listenToTopLevelEvent(
1115 + ((type: any): DOMTopLevelEventType),
1116 + rootContainerInstance,
1117 + listenerMap,
1118 + passive,
1119 + priority,
1120 + );
1121 +}
1122 +
1123 export function mountEventListener(listener: ReactDOMListener): void {
1124 if (enableUseEventAPI) {
1125 const {target} = listener;
packages/react-dom/src/events/SimpleEventPlugin.js
+1 -1
@@ -191,7 +191,7 @@ const SimpleEventPlugin: PluginModule<MouseEvent> = {
191 nativeEvent,
192 nativeEventTarget,
193 );
194 - accumulateTwoPhaseListeners(event);
194 + accumulateTwoPhaseListeners(event, true);
195 return event;
196 },
197 };
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+525
@@ -1074,5 +1074,530 @@ describe('DOMModernPluginEventSystem', () => {
1074 expect(listenerMaps.length).toEqual(2);
1075 expect(listenerMaps[0]).toEqual(listenerMaps[1]);
1076 });
1077 +
1078 + it('can render correctly with the ReactDOMServer', () => {
1079 + const clickEvent = jest.fn();
1080 +
1081 + function Test() {
1082 + const divRef = React.useRef(null);
1083 + const click = ReactDOM.unstable_useEvent('click');
1084 +
1085 + React.useEffect(() => {
1086 + click.setListener(divRef.current, clickEvent);
1087 + });
1088 +
1089 + return <div ref={divRef}>Hello world</div>;
1090 + }
1091 + const output = ReactDOMServer.renderToString(<Test />);
1092 + expect(output).toBe(`<div data-reactroot="">Hello world</div>`);
1093 + });
1094 +
1095 + it('can render correctly with the ReactDOMServer hydration', () => {
1096 + const clickEvent = jest.fn();
1097 + const spanRef = React.createRef();
1098 +
1099 + function Test() {
1100 + const click = ReactDOM.unstable_useEvent('click');
1101 +
1102 + React.useEffect(() => {
1103 + click.setListener(spanRef.current, clickEvent);
1104 + });
1105 +
1106 + return (
1107 + <div>
1108 + <span ref={spanRef}>Hello world</span>
1109 + </div>
1110 + );
1111 + }
1112 + const output = ReactDOMServer.renderToString(<Test />);
1113 + expect(output).toBe(
1114 + `<div data-reactroot=""><span>Hello world</span></div>`,
1115 + );
1116 + container.innerHTML = output;
1117 + ReactDOM.hydrate(<Test />, container);
1118 + Scheduler.unstable_flushAll();
1119 + dispatchClickEvent(spanRef.current);
1120 + expect(clickEvent).toHaveBeenCalledTimes(1);
1121 + });
1122 +
1123 + it('should correctly work for a basic "click" listener', () => {
1124 + let log = [];
1125 + const clickEvent = jest.fn(event => {
1126 + log.push({
1127 + eventPhase: event.eventPhase,
1128 + type: event.type,
1129 + currentTarget: event.currentTarget,
1130 + target: event.target,
1131 + });
1132 + });
1133 + const divRef = React.createRef();
1134 + const buttonRef = React.createRef();
1135 +
1136 + function Test() {
1137 + const click = ReactDOM.unstable_useEvent('click');
1138 +
1139 + React.useEffect(() => {
1140 + click.setListener(buttonRef.current, clickEvent);
1141 + });
1142 +
1143 + return (
1144 + <button ref={buttonRef}>
1145 + <div ref={divRef}>Click me!</div>
1146 + </button>
1147 + );
1148 + }
1149 +
1150 + ReactDOM.render(<Test />, container);
1151 + Scheduler.unstable_flushAll();
1152 +
1153 + expect(container.innerHTML).toBe('<button><div>Click me!</div></button>');
1154 +
1155 + // Clicking the button should trigger the event callback
1156 + let divElement = divRef.current;
1157 + dispatchClickEvent(divElement);
1158 + expect(log).toEqual([
1159 + {
1160 + eventPhase: 3,
1161 + type: 'click',
1162 + currentTarget: buttonRef.current,
1163 + target: divRef.current,
1164 + },
1165 + ]);
1166 + expect(clickEvent).toBeCalledTimes(1);
1167 +
1168 + // Unmounting the container and clicking should not work
1169 + ReactDOM.render(null, container);
1170 + Scheduler.unstable_flushAll();
1171 +
1172 + dispatchClickEvent(divElement);
1173 + expect(clickEvent).toBeCalledTimes(1);
1174 +
1175 + // Re-rendering the container and clicking should work
1176 + ReactDOM.render(<Test />, container);
1177 + Scheduler.unstable_flushAll();
1178 +
1179 + divElement = divRef.current;
1180 + dispatchClickEvent(divElement);
1181 + expect(clickEvent).toBeCalledTimes(2);
1182 +
1183 + log = [];
1184 +
1185 + // Clicking the button should also work
1186 + let buttonElement = buttonRef.current;
1187 + dispatchClickEvent(buttonElement);
1188 + expect(log).toEqual([
1189 + {
1190 + eventPhase: 3,
1191 + type: 'click',
1192 + currentTarget: buttonRef.current,
1193 + target: buttonRef.current,
1194 + },
1195 + ]);
1196 +
1197 + function Test2({clickEvent2}) {
1198 + const click = ReactDOM.unstable_useEvent('click', clickEvent2);
1199 +
1200 + React.useEffect(() => {
1201 + click.setListener(buttonRef.current, clickEvent2);
1202 + });
1203 +
1204 + return (
1205 + <button ref={buttonRef}>
1206 + <div ref={divRef}>Click me!</div>
1207 + </button>
1208 + );
1209 + }
1210 +
1211 + let clickEvent2 = jest.fn();
1212 + ReactDOM.render(<Test2 clickEvent2={clickEvent2} />, container);
1213 + Scheduler.unstable_flushAll();
1214 +
1215 + divElement = divRef.current;
1216 + dispatchClickEvent(divElement);
1217 + expect(clickEvent2).toBeCalledTimes(1);
1218 +
1219 + // Reset the function we pass in, so it's different
1220 + clickEvent2 = jest.fn();
1221 + ReactDOM.render(<Test2 clickEvent2={clickEvent2} />, container);
1222 + Scheduler.unstable_flushAll();
1223 +
1224 + divElement = divRef.current;
1225 + dispatchClickEvent(divElement);
1226 + expect(clickEvent2).toBeCalledTimes(1);
1227 + });
1228 +
1229 + it('should correctly work for setting and clearing a basic "click" listener', () => {
1230 + const clickEvent = jest.fn();
1231 + const divRef = React.createRef();
1232 + const buttonRef = React.createRef();
1233 +
1234 + function Test({off}) {
1235 + const click = ReactDOM.unstable_useEvent('click');
1236 +
1237 + React.useEffect(() => {
1238 + click.setListener(buttonRef.current, clickEvent);
1239 + });
1240 +
1241 + React.useEffect(() => {
1242 + if (off) {
1243 + click.setListener(buttonRef.current, null);
1244 + }
1245 + }, [off]);
1246 +
1247 + return (
1248 + <button ref={buttonRef}>
1249 + <div ref={divRef}>Click me!</div>
1250 + </button>
1251 + );
1252 + }
1253 +
1254 + ReactDOM.render(<Test off={false} />, container);
1255 + Scheduler.unstable_flushAll();
1256 +
1257 + let divElement = divRef.current;
1258 + dispatchClickEvent(divElement);
1259 + expect(clickEvent).toBeCalledTimes(1);
1260 +
1261 + // The listener should get unmounted in the second effect
1262 + ReactDOM.render(<Test off={true} />, container);
1263 + Scheduler.unstable_flushAll();
1264 +
1265 + clickEvent.mockClear();
1266 +
1267 + divElement = divRef.current;
1268 + dispatchClickEvent(divElement);
1269 + expect(clickEvent).toBeCalledTimes(0);
1270 + });
1271 +
1272 + it('handle propagation of click events', () => {
1273 + const buttonRef = React.createRef();
1274 + const divRef = React.createRef();
1275 + const log = [];
1276 + const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
1277 + const onClickCapture = jest.fn(e =>
1278 + log.push(['capture', e.currentTarget]),
1279 + );
1280 +
1281 + function Test() {
1282 + const click = ReactDOM.unstable_useEvent('click');
1283 + const clickCapture = ReactDOM.unstable_useEvent('click', {
1284 + capture: true,
1285 + });
1286 +
1287 + React.useEffect(() => {
1288 + click.setListener(buttonRef.current, onClick);
1289 + clickCapture.setListener(buttonRef.current, onClickCapture);
1290 + click.setListener(divRef.current, onClick);
1291 + clickCapture.setListener(divRef.current, onClickCapture);
1292 + });
1293 +
1294 + return (
1295 + <button ref={buttonRef}>
1296 + <div ref={divRef}>Click me!</div>
1297 + </button>
1298 + );
1299 + }
1300 +
1301 + ReactDOM.render(<Test />, container);
1302 + Scheduler.unstable_flushAll();
1303 +
1304 + let buttonElement = buttonRef.current;
1305 + dispatchClickEvent(buttonElement);
1306 + expect(onClick).toHaveBeenCalledTimes(1);
1307 + expect(onClickCapture).toHaveBeenCalledTimes(1);
1308 + expect(log[0]).toEqual(['capture', buttonElement]);
1309 + expect(log[1]).toEqual(['bubble', buttonElement]);
1310 +
1311 + let divElement = divRef.current;
1312 + dispatchClickEvent(divElement);
1313 + expect(onClick).toHaveBeenCalledTimes(3);
1314 + expect(onClickCapture).toHaveBeenCalledTimes(3);
1315 + expect(log[2]).toEqual(['capture', buttonElement]);
1316 + expect(log[3]).toEqual(['capture', divElement]);
1317 + expect(log[4]).toEqual(['bubble', divElement]);
1318 + expect(log[5]).toEqual(['bubble', buttonElement]);
1319 + });
1320 +
1321 + it('should correctly work for a basic "click" listener on the outer target', () => {
1322 + const log = [];
1323 + const clickEvent = jest.fn(event => {
1324 + log.push({
1325 + eventPhase: event.eventPhase,
1326 + type: event.type,
1327 + currentTarget: event.currentTarget,
1328 + target: event.target,
1329 + });
1330 + });
1331 + const divRef = React.createRef();
1332 + const buttonRef = React.createRef();
1333 +
1334 + function Test() {
1335 + const click = ReactDOM.unstable_useEvent('click');
1336 +
1337 + React.useEffect(() => {
1338 + click.setListener(divRef.current, clickEvent);
1339 + });
1340 +
1341 + return (
1342 + <button ref={buttonRef}>
1343 + <div ref={divRef}>Click me!</div>
1344 + </button>
1345 + );
1346 + }
1347 +
1348 + ReactDOM.render(<Test />, container);
1349 + Scheduler.unstable_flushAll();
1350 +
1351 + expect(container.innerHTML).toBe('<button><div>Click me!</div></button>');
1352 +
1353 + // Clicking the button should trigger the event callback
1354 + let divElement = divRef.current;
1355 + dispatchClickEvent(divElement);
1356 + expect(log).toEqual([
1357 + {
1358 + eventPhase: 3,
1359 + type: 'click',
1360 + currentTarget: divRef.current,
1361 + target: divRef.current,
1362 + },
1363 + ]);
1364 +
1365 + // Unmounting the container and clicking should not work
1366 + ReactDOM.render(null, container);
1367 + dispatchClickEvent(divElement);
1368 + expect(clickEvent).toBeCalledTimes(1);
1369 +
1370 + // Re-rendering the container and clicking should work
1371 + ReactDOM.render(<Test />, container);
1372 + Scheduler.unstable_flushAll();
1373 +
1374 + divElement = divRef.current;
1375 + dispatchClickEvent(divElement);
1376 + expect(clickEvent).toBeCalledTimes(2);
1377 +
1378 + // Clicking the button should not work
1379 + let buttonElement = buttonRef.current;
1380 + dispatchClickEvent(buttonElement);
1381 + expect(clickEvent).toBeCalledTimes(2);
1382 + });
1383 +
1384 + it('should correctly handle many nested target listeners', () => {
1385 + const buttonRef = React.createRef();
1386 + const targetListener1 = jest.fn();
1387 + const targetListener2 = jest.fn();
1388 + const targetListener3 = jest.fn();
1389 + const targetListener4 = jest.fn();
1390 +
1391 + function Test() {
1392 + const click1 = ReactDOM.unstable_useEvent('click', {capture: true});
1393 + const click2 = ReactDOM.unstable_useEvent('click', {capture: true});
1394 + const click3 = ReactDOM.unstable_useEvent('click');
1395 + const click4 = ReactDOM.unstable_useEvent('click');
1396 +
1397 + React.useEffect(() => {
1398 + click1.setListener(buttonRef.current, targetListener1);
1399 + click2.setListener(buttonRef.current, targetListener2);
1400 + click3.setListener(buttonRef.current, targetListener3);
1401 + click4.setListener(buttonRef.current, targetListener4);
1402 + });
1403 +
1404 + return <button ref={buttonRef}>Click me!</button>;
1405 + }
1406 +
1407 + ReactDOM.render(<Test />, container);
1408 + Scheduler.unstable_flushAll();
1409 +
1410 + let buttonElement = buttonRef.current;
1411 + dispatchClickEvent(buttonElement);
1412 +
1413 + expect(targetListener1).toHaveBeenCalledTimes(1);
1414 + expect(targetListener2).toHaveBeenCalledTimes(1);
1415 + expect(targetListener3).toHaveBeenCalledTimes(1);
1416 + expect(targetListener4).toHaveBeenCalledTimes(1);
1417 +
1418 + function Test2() {
1419 + const click1 = ReactDOM.unstable_useEvent('click');
1420 + const click2 = ReactDOM.unstable_useEvent('click');
1421 + const click3 = ReactDOM.unstable_useEvent('click');
1422 + const click4 = ReactDOM.unstable_useEvent('click');
1423 +
1424 + React.useEffect(() => {
1425 + click1.setListener(buttonRef.current, targetListener1);
1426 + click2.setListener(buttonRef.current, targetListener2);
1427 + click3.setListener(buttonRef.current, targetListener3);
1428 + click4.setListener(buttonRef.current, targetListener4);
1429 + });
1430 +
1431 + return <button ref={buttonRef}>Click me!</button>;
1432 + }
1433 +
1434 + ReactDOM.render(<Test2 />, container);
1435 + Scheduler.unstable_flushAll();
1436 +
1437 + buttonElement = buttonRef.current;
1438 + dispatchClickEvent(buttonElement);
1439 + expect(targetListener1).toHaveBeenCalledTimes(2);
1440 + expect(targetListener2).toHaveBeenCalledTimes(2);
1441 + expect(targetListener3).toHaveBeenCalledTimes(2);
1442 + expect(targetListener4).toHaveBeenCalledTimes(2);
1443 + });
1444 +
1445 + it('should correctly handle stopPropagation corrrectly for target events', () => {
1446 + const buttonRef = React.createRef();
1447 + const divRef = React.createRef();
1448 + let clickEvent = jest.fn();
1449 +
1450 + function Test() {
1451 + const click1 = ReactDOM.unstable_useEvent('click', {
1452 + bind: buttonRef,
1453 + });
1454 + const click2 = ReactDOM.unstable_useEvent('click');
1455 +
1456 + React.useEffect(() => {
1457 + click1.setListener(buttonRef.current, clickEvent);
1458 + click2.setListener(divRef.current, e => {
1459 + e.stopPropagation();
1460 + });
1461 + });
1462 +
1463 + return (
1464 + <button ref={buttonRef}>
1465 + <div ref={divRef}>Click me!</div>
1466 + </button>
1467 + );
1468 + }
1469 +
1470 + ReactDOM.render(<Test />, container);
1471 + Scheduler.unstable_flushAll();
1472 +
1473 + let divElement = divRef.current;
1474 + dispatchClickEvent(divElement);
1475 + expect(clickEvent).toHaveBeenCalledTimes(0);
1476 + });
1477 +
1478 + it('should correctly handle stopPropagation corrrectly for many target events', () => {
1479 + const buttonRef = React.createRef();
1480 + const targetListerner1 = jest.fn(e => e.stopPropagation());
1481 + const targetListerner2 = jest.fn(e => e.stopPropagation());
1482 + const targetListerner3 = jest.fn(e => e.stopPropagation());
1483 + const targetListerner4 = jest.fn(e => e.stopPropagation());
1484 +
1485 + function Test() {
1486 + const click1 = ReactDOM.unstable_useEvent('click');
1487 + const click2 = ReactDOM.unstable_useEvent('click');
1488 + const click3 = ReactDOM.unstable_useEvent('click');
1489 + const click4 = ReactDOM.unstable_useEvent('click');
1490 +
1491 + React.useEffect(() => {
1492 + click1.setListener(buttonRef.current, targetListerner1);
1493 + click2.setListener(buttonRef.current, targetListerner2);
1494 + click3.setListener(buttonRef.current, targetListerner3);
1495 + click4.setListener(buttonRef.current, targetListerner4);
1496 + });
1497 +
1498 + return <button ref={buttonRef}>Click me!</button>;
1499 + }
1500 +
1501 + ReactDOM.render(<Test />, container);
1502 + Scheduler.unstable_flushAll();
1503 +
1504 + let buttonElement = buttonRef.current;
1505 + dispatchClickEvent(buttonElement);
1506 + expect(targetListerner1).toHaveBeenCalledTimes(1);
1507 + expect(targetListerner2).toHaveBeenCalledTimes(1);
1508 + expect(targetListerner3).toHaveBeenCalledTimes(1);
1509 + expect(targetListerner4).toHaveBeenCalledTimes(1);
1510 + });
1511 +
1512 + it('should correctly handle stopPropagation for mixed capture/bubbling target listeners', () => {
1513 + const buttonRef = React.createRef();
1514 + const targetListerner1 = jest.fn(e => e.stopPropagation());
1515 + const targetListerner2 = jest.fn(e => e.stopPropagation());
1516 + const targetListerner3 = jest.fn(e => e.stopPropagation());
1517 + const targetListerner4 = jest.fn(e => e.stopPropagation());
1518 +
1519 + function Test() {
1520 + const click1 = ReactDOM.unstable_useEvent('click', {capture: true});
1521 + const click2 = ReactDOM.unstable_useEvent('click', {capture: true});
1522 + const click3 = ReactDOM.unstable_useEvent('click');
1523 + const click4 = ReactDOM.unstable_useEvent('click');
1524 +
1525 + React.useEffect(() => {
1526 + click1.setListener(buttonRef.current, targetListerner1);
1527 + click2.setListener(buttonRef.current, targetListerner2);
1528 + click3.setListener(buttonRef.current, targetListerner3);
1529 + click4.setListener(buttonRef.current, targetListerner4);
1530 + });
1531 +
1532 + return <button ref={buttonRef}>Click me!</button>;
1533 + }
1534 +
1535 + ReactDOM.render(<Test />, container);
1536 + Scheduler.unstable_flushAll();
1537 +
1538 + let buttonElement = buttonRef.current;
1539 + dispatchClickEvent(buttonElement);
1540 + expect(targetListerner1).toHaveBeenCalledTimes(1);
1541 + expect(targetListerner2).toHaveBeenCalledTimes(1);
1542 + expect(targetListerner3).toHaveBeenCalledTimes(1);
1543 + expect(targetListerner4).toHaveBeenCalledTimes(1);
1544 + });
1545 +
1546 + it.experimental('should work with concurrent mode updates', async () => {
1547 + const log = [];
1548 + const ref = React.createRef();
1549 +
1550 + function Test({counter}) {
1551 + const click = ReactDOM.unstable_useEvent('click');
1552 +
1553 + React.useLayoutEffect(() => {
1554 + click.setListener(ref.current, () => {
1555 + log.push({counter});
1556 + });
1557 + });
1558 +
1559 + Scheduler.unstable_yieldValue('Test');
1560 + return <button ref={ref}>Press me</button>;
1561 + }
1562 +
1563 + let root = ReactDOM.createRoot(container);
1564 + root.render(<Test counter={0} />);
1565 +
1566 + // Dev double-render
1567 + if (__DEV__) {
1568 + expect(Scheduler).toFlushAndYield(['Test', 'Test']);
1569 + } else {
1570 + expect(Scheduler).toFlushAndYield(['Test']);
1571 + }
1572 +
1573 + // Click the button
1574 + dispatchClickEvent(ref.current);
1575 + expect(log).toEqual([{counter: 0}]);
1576 +
1577 + // Clear log
1578 + log.length = 0;
1579 +
1580 + // Increase counter
1581 + root.render(<Test counter={1} />);
1582 + // Yield before committing
1583 + // Dev double-render
1584 + if (__DEV__) {
1585 + expect(Scheduler).toFlushAndYieldThrough(['Test', 'Test']);
1586 + } else {
1587 + expect(Scheduler).toFlushAndYieldThrough(['Test']);
1588 + }
1589 +
1590 + // Click the button again
1591 + dispatchClickEvent(ref.current);
1592 + expect(log).toEqual([{counter: 0}]);
1593 +
1594 + // Clear log
1595 + log.length = 0;
1596 +
1597 + // Commit
1598 + expect(Scheduler).toFlushAndYield([]);
1599 + dispatchClickEvent(ref.current);
1600 + expect(log).toEqual([{counter: 1}]);
1601 + });
1602 });
1603 });
packages/react-dom/src/events/accumulateTwoPhaseListeners.js
+32 -1
@@ -9,11 +9,15 @@
9
10 import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
11
12 -import getListener from 'legacy-events/getListener';
12 import {HostComponent} from 'shared/ReactWorkTags';
13 +import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
14 +
15 +import getListener from 'legacy-events/getListener';
16 +import {getListenersFromTarget} from '../client/ReactDOMComponentTree';
17
18 export default function accumulateTwoPhaseListeners(
19 event: ReactSyntheticEvent,
20 + accumulateUseEventListeners?: boolean,
21 ): void {
22 const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
23 if (phasedRegistrationNames == null) {
@@ -28,6 +32,33 @@ export default function accumulateTwoPhaseListeners(
32 while (node !== null) {
33 // We only care for listeners that are on HostComponents (i.e. <div>)
34 if (node.tag === HostComponent) {
35 + // For useEvent listenrs
36 + if (enableUseEventAPI && accumulateUseEventListeners) {
37 + // useEvent event listeners
38 + const instance = node.stateNode;
39 + const targetType = event.type;
40 + const listeners = getListenersFromTarget(instance);
41 +
42 + if (listeners !== null) {
43 + const listenersArr = Array.from(listeners);
44 + for (let i = 0; i < listenersArr.length; i++) {
45 + const listener = listenersArr[i];
46 + const {
47 + callback,
48 + event: {capture, type},
49 + } = listener;
50 + if (type === targetType) {
51 + if (capture === true) {
52 + dispatchListeners.unshift(callback);
53 + dispatchInstances.unshift(node);
54 + } else {
55 + dispatchListeners.push(callback);
56 + dispatchInstances.push(node);
57 + }
58 + }
59 + }
60 + }
61 + }
62 // Standard React on* listeners, i.e. onClick prop
63 const captureListener = getListener(node, captured);
64 if (captureListener != null) {
packages/react-native-renderer/src/ReactFabricHostConfig.js
+4
@@ -474,6 +474,10 @@ export function beforeRemoveInstance(instance: any) {
474 // noop
475 }
476
477 +export function registerEvent(event: any, rootContainerInstance: Container) {
478 + throw new Error('Not yet implemented.');
479 +}
480 +
481 export function mountEventListener(listener: any) {
482 throw new Error('Not yet implemented.');
483 }
packages/react-native-renderer/src/ReactNativeHostConfig.js
+4
@@ -518,6 +518,10 @@ export function beforeRemoveInstance(instance: any) {
518 // noop
519 }
520
521 +export function registerEvent(event: any, rootContainerInstance: Container) {
522 + throw new Error('Not yet implemented.');
523 +}
524 +
525 export function mountEventListener(listener: any) {
526 throw new Error('Not yet implemented.');
527 }
packages/react-reconciler/src/ReactFiberCommitWork.js
+3
@@ -36,6 +36,7 @@ import {
36 enableSuspenseCallback,
37 enableScopeAPI,
38 runAllPassiveEffectDestroysBeforeCreates,
39 + enableUseEventAPI,
40 } from 'shared/ReactFeatureFlags';
41 import {
42 FunctionComponent,
@@ -1053,6 +1054,8 @@ function commitUnmount(
1054 case HostComponent: {
1055 if (enableDeprecatedFlareAPI) {
1056 unmountDeprecatedResponderListeners(current);
1057 + }
1058 + if (enableDeprecatedFlareAPI || enableUseEventAPI) {
1059 beforeRemoveInstance(current.stateNode);
1060 }
1061 safelyDetachRef(current);
packages/react-reconciler/src/ReactFiberHooks.js
+86 -3
@@ -24,6 +24,7 @@ import type {FiberRoot} from './ReactFiberRoot';
24 import type {
25 ReactListenerEvent,
26 ReactListenerMap,
27 + ReactListener,
28 } from './ReactFiberHostConfig';
29
30 import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -53,6 +54,12 @@ import {
54 markRenderEventTimeAndConfig,
55 markUnprocessedUpdateTime,
56 } from './ReactFiberWorkLoop';
57 +import {
58 + registerEvent,
59 + mountEventListener as mountHostEventListener,
60 + unmountEventListener as unmountHostEventListener,
61 + validateEventListenerTarget,
62 +} from './ReactFiberHostConfig';
63
64 import invariant from 'shared/invariant';
65 import getComponentName from 'shared/getComponentName';
@@ -73,6 +80,7 @@ import {
80 setWorkInProgressVersion,
81 warnAboutMultipleRenderersDEV,
82 } from './ReactMutableSource';
83 +import {getRootHostContainer} from './ReactFiberHostContext';
84
85 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
86
@@ -1627,18 +1635,93 @@ function dispatchAction<S, A>(
1635
1636 const noOpMount = () => {};
1637
1638 +function validateNotInFunctionRender(): boolean {
1639 + if (currentlyRenderingFiber === null) {
1640 + return true;
1641 + }
1642 + if (__DEV__) {
1643 + console.warn(
1644 + 'Event listener methods from useEvent() cannot be called during render.' +
1645 + ' These methods should be called in an effect or event callback outside the render.',
1646 + );
1647 + }
1648 + return false;
1649 +}
1650 +
1651 +function createReactListener(
1652 + event: ReactListenerEvent,
1653 + callback: Event => void,
1654 + target: EventTarget,
1655 + destroy: Node => void,
1656 +): ReactListener {
1657 + return {
1658 + callback,
1659 + destroy,
1660 + event,
1661 + target,
1662 + };
1663 +}
1664 +
1665 function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
1666 if (enableUseEventAPI) {
1667 const hook = mountWorkInProgressHook();
1668 + const listenerMap: Map<EventTarget, ReactListener> = new Map();
1669 + const rootContainerInstance = getRootHostContainer();
1670 +
1671 + // Register the event to the current root to ensure event
1672 + // replaying can pick up the event ahead of time.
1673 + registerEvent(event, rootContainerInstance);
1674
1675 const clear = () => {
1635 - // TODO
1676 + if (validateNotInFunctionRender()) {
1677 + const listeners = Array.from(listenerMap.values());
1678 + for (let i = 0; i < listeners.length; i++) {
1679 + unmountHostEventListener(listeners[i]);
1680 + }
1681 + listenerMap.clear();
1682 + }
1683 + };
1684 +
1685 + const destroy = (target: Node) => {
1686 + // We don't need to call detachListenerFromInstance
1687 + // here as this method should only ever be called
1688 + // from renderers that need to remove the instance
1689 + // from the map representing an instance that still
1690 + // holds a reference to the listenerMap. This means
1691 + // things like "window" listeners on ReactDOM should
1692 + // never enter this call path as the the instance in
1693 + // those cases would be that of "window", which
1694 + // should be handled via an optimized route in the
1695 + // renderer, making less overhead here. If we change
1696 + // this heuristic we should update this path to make
1697 + // sure we call detachListenerFromInstance.
1698 + listenerMap.delete(target);
1699 };
1700
1701 const reactListenerMap: ReactListenerMap = {
1702 clear,
1640 - setListener(instance: EventTarget, callback: ?(Event) => void): void {
1641 - // TODO
1703 + setListener(target: EventTarget, callback: ?(Event) => void): void {
1704 + if (
1705 + validateNotInFunctionRender() &&
1706 + validateEventListenerTarget(target, callback)
1707 + ) {
1708 + let listener = listenerMap.get(target);
1709 + if (listener === undefined) {
1710 + if (callback == null) {
1711 + return;
1712 + }
1713 + listener = createReactListener(event, callback, target, destroy);
1714 + listenerMap.set(target, listener);
1715 + } else {
1716 + if (callback == null) {
1717 + listenerMap.delete(target);
1718 + unmountHostEventListener(listener);
1719 + return;
1720 + }
1721 + listener.callback = callback;
1722 + }
1723 + mountHostEventListener(listener);
1724 + }
1725 },
1726 };
1727 // In order to clear up upon the hook unmounting,
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+1
@@ -76,6 +76,7 @@ export const shouldUpdateFundamentalComponent =
76 $$$hostConfig.shouldUpdateFundamentalComponent;
77 export const getInstanceFromNode = $$$hostConfig.getInstanceFromNode;
78 export const beforeRemoveInstance = $$$hostConfig.beforeRemoveInstance;
79 +export const registerEvent = $$$hostConfig.registerEvent;
80 export const mountEventListener = $$$hostConfig.mountEventListener;
81 export const unmountEventListener = $$$hostConfig.unmountEventListener;
82 export const validateEventListenerTarget =
packages/react-test-renderer/src/ReactTestHostConfig.js
+4
@@ -380,6 +380,10 @@ export function beforeRemoveInstance(instance: any) {
380 // noop
381 }
382
383 +export function registerEvent(event: any, rootContainerInstance: Container) {
384 + throw new Error('Not yet implemented.');
385 +}
386 +
387 export function mountEventListener(listener: any) {
388 throw new Error('Not yet implemented.');
389 }