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
}