Revert "Check for event listener in props instead of bank (#8192)" (#8239)
This reverts commit a878c3056d2df0cf4d7f3b5e2db6031d598ef23f. The previous stuff that this builds on didn't successfully land fully. Since I want to measure this in isolation, I'll revert this for now.
Sebastian Markbåge committed
Nov 8, 2016 at 11:32 UTC
b2eaafaf3b19d3c74a30f0e6dc9d4a51bd91ba2f
15 files changed
+491
-239
scripts/fiber/tests-failing.txt
+5
-3
@@ -147,6 +147,8 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
147
* should validate against invalid styles
148
* should track input values
149
* should track textarea values
150
+* should execute custom event plugin listening behavior
151
+* should handle null and missing properly with event hooks
152
* should warn for children on void elements
153
* should support custom elements which extend native elements
154
* should warn against children for void elements
@@ -155,6 +157,7 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
157
* should warn about contentEditable and children
158
* should validate against invalid styles
159
* should report component containing invalid styles
160
+* should clean up listeners
161
* should clean up input value tracking
162
* should clean up input textarea tracking
163
* should warn about the `onScroll` issue when unsupported (IE8)
@@ -379,11 +382,13 @@ src/renderers/dom/stack/server/__tests__/ReactServerRendering-test.js
382
* should generate simple markup for self-closing tags
383
* should generate simple markup for attribute with `>` symbol
384
* should generate comment markup for component returns null
385
+* should not register event listeners
386
* should render composite components
387
* should only execute certain lifecycle methods
388
* should have the correct mounting behavior
389
* should not put checksum and React ID on components
390
* should not put checksum and React ID on text components
391
+* should not register event listeners
392
* should only execute certain lifecycle methods
393
* allows setState in componentWillMount without using DOM
394
* renders components with different batching strategies
@@ -498,9 +503,6 @@ src/renderers/shared/shared/__tests__/ReactTreeTraversal-test.js
503
* should leave to the window
504
* should leave to the window from the shallowest
505
501
-src/renderers/shared/shared/event/__tests__/EventPluginHub-test.js
502
-* should prevent non-function listeners, at dispatch
503
-
506
src/renderers/shared/stack/reconciler/__tests__/ReactChildReconciler-test.js
507
* warns for duplicated keys
508
* warns for duplicated keys with component stack info
scripts/fiber/tests-passing.txt
+4
@@ -955,6 +955,9 @@ src/renderers/shared/shared/__tests__/ReactTreeTraversal-test.js
955
* should not traverse if enter/leave the same node
956
* should determine the first common ancestor correctly
957
958
+src/renderers/shared/shared/event/__tests__/EventPluginHub-test.js
959
+* should prevent non-function listeners
960
+
961
src/renderers/shared/shared/event/__tests__/EventPluginRegistry-test.js
962
* should be able to inject ordering before plugins
963
* should be able to inject plugins before and after ordering
@@ -1059,6 +1062,7 @@ src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js
1062
* propagates errors on retry on mounting
1063
* propagates errors inside boundary during componentWillMount
1064
* propagates errors inside boundary while rendering error state
1065
+* does not register event handlers for unmounted children
1066
* does not call componentWillUnmount when aborting initial mount
1067
* resets refs if mounting aborts
1068
* successfully mounts if no error occurs
src/renderers/dom/shared/ReactBrowserEventEmitter.js
+10
@@ -163,6 +163,16 @@ function getListeningForDocument(mountAt) {
163
return alreadyListeningTo[mountAt[topListenersIDKey]];
164
}
165
166
+/**
167
+ * `ReactBrowserEventEmitter` is used to attach top-level event listeners. For
168
+ * example:
169
+ *
170
+ * EventPluginHub.putListener('myID', 'onClick', myFunction);
171
+ *
172
+ * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
173
+ *
174
+ * @internal
175
+ */
176
var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, {
177
178
/**
src/renderers/dom/shared/__tests__/ReactBrowserEventEmitter-test.js
+126
-175
@@ -15,9 +15,8 @@ var EventListener;
15
var EventPluginHub;
16
var EventPluginRegistry;
17
var React;
18
-var ReactDOM;
19
-var ReactDOMComponentTree;
18
var ReactBrowserEventEmitter;
19
+var ReactDOMComponentTree;
20
var ReactTestUtils;
21
var TapEventPlugin;
22
@@ -44,17 +43,18 @@ var GRANDPARENT;
43
var PARENT;
44
var CHILD;
45
47
-var getListener;
48
-var putListener;
49
-var deleteAllListeners;
50
-
46
function registerSimpleTestHandler() {
52
- putListener(CHILD, ON_CLICK_KEY, LISTENER);
53
- var listener = getListener(CHILD, ON_CLICK_KEY);
47
+ EventPluginHub.putListener(getInternal(CHILD), ON_CLICK_KEY, LISTENER);
48
+ var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY);
49
expect(listener).toEqual(LISTENER);
55
- return getListener(CHILD, ON_CLICK_KEY);
50
+ return EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY);
51
+}
52
+
53
+function getInternal(node) {
54
+ return ReactDOMComponentTree.getInstanceFromNode(node);
55
}
56
57
+
58
describe('ReactBrowserEventEmitter', () => {
59
beforeEach(() => {
60
jest.resetModuleRegistry();
@@ -63,67 +63,18 @@ describe('ReactBrowserEventEmitter', () => {
63
EventPluginHub = require('EventPluginHub');
64
EventPluginRegistry = require('EventPluginRegistry');
65
React = require('React');
66
- ReactDOM = require('ReactDOM');
67
- ReactDOMComponentTree = require('ReactDOMComponentTree');
66
ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
67
+ ReactDOMComponentTree = require('ReactDOMComponentTree');
68
ReactTestUtils = require('ReactTestUtils');
69
TapEventPlugin = require('TapEventPlugin');
70
72
- var container = document.createElement('div');
73
-
74
- var GRANDPARENT_PROPS = {};
75
- var PARENT_PROPS = {};
76
- var CHILD_PROPS = {};
77
-
78
- function renderTree() {
79
- ReactDOM.render(
80
- <div ref={(c) => GRANDPARENT = c} {...GRANDPARENT_PROPS}>
81
- <div ref={(c) => PARENT = c} {...PARENT_PROPS}>
82
- <div ref={(c) => CHILD = c} {...CHILD_PROPS} />
83
- </div>
84
- </div>,
85
- container
86
- );
87
- }
88
-
89
- renderTree();
90
-
91
- getListener = function(node, eventName) {
92
- var inst = ReactDOMComponentTree.getInstanceFromNode(node);
93
- return EventPluginHub.getListener(
94
- inst,
95
- eventName
96
- );
97
- };
98
- putListener = function(node, eventName, listener) {
99
- switch (node) {
100
- case CHILD:
101
- CHILD_PROPS[eventName] = listener;
102
- break;
103
- case PARENT:
104
- PARENT_PROPS[eventName] = listener;
105
- break;
106
- case GRANDPARENT:
107
- GRANDPARENT_PROPS[eventName] = listener;
108
- break;
109
- }
110
- // Rerender with new event listeners
111
- renderTree();
112
- };
113
- deleteAllListeners = function(node) {
114
- switch (node) {
115
- case CHILD:
116
- CHILD_PROPS = {};
117
- break;
118
- case PARENT:
119
- PARENT_PROPS = {};
120
- break;
121
- case GRANDPARENT:
122
- GRANDPARENT_PROPS = {};
123
- break;
124
- }
125
- renderTree();
126
- };
71
+ ReactTestUtils.renderIntoDocument(
72
+ <div ref={(c) => GRANDPARENT = c}>
73
+ <div ref={(c) => PARENT = c}>
74
+ <div ref={(c) => CHILD = c} />
75
+ </div>
76
+ </div>
77
+ );
78
79
idCallOrder = [];
80
tapMoveThreshold = TapEventPlugin.tapMoveThreshold;
@@ -134,20 +85,20 @@ describe('ReactBrowserEventEmitter', () => {
85
86
it('should store a listener correctly', () => {
87
registerSimpleTestHandler();
137
- var listener = getListener(CHILD, ON_CLICK_KEY);
88
+ var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY);
89
expect(listener).toBe(LISTENER);
90
});
91
92
it('should retrieve a listener correctly', () => {
93
registerSimpleTestHandler();
143
- var listener = getListener(CHILD, ON_CLICK_KEY);
94
+ var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY);
95
expect(listener).toEqual(LISTENER);
96
});
97
98
it('should clear all handlers when asked to', () => {
99
registerSimpleTestHandler();
149
- deleteAllListeners(CHILD);
150
- var listener = getListener(CHILD, ON_CLICK_KEY);
100
+ EventPluginHub.deleteAllListeners(getInternal(CHILD));
101
+ var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY);
102
expect(listener).toBe(undefined);
103
});
104
@@ -171,153 +122,153 @@ describe('ReactBrowserEventEmitter', () => {
122
);
123
124
it('should bubble simply', () => {
174
- putListener(
175
- CHILD,
125
+ EventPluginHub.putListener(
126
+ getInternal(CHILD),
127
ON_CLICK_KEY,
177
- recordID.bind(null, CHILD)
128
+ recordID.bind(null, getInternal(CHILD))
129
);
179
- putListener(
180
- PARENT,
130
+ EventPluginHub.putListener(
131
+ getInternal(PARENT),
132
ON_CLICK_KEY,
182
- recordID.bind(null, PARENT)
133
+ recordID.bind(null, getInternal(PARENT))
134
);
184
- putListener(
185
- GRANDPARENT,
135
+ EventPluginHub.putListener(
136
+ getInternal(GRANDPARENT),
137
ON_CLICK_KEY,
187
- recordID.bind(null, GRANDPARENT)
138
+ recordID.bind(null, getInternal(GRANDPARENT))
139
);
140
ReactTestUtils.Simulate.click(CHILD);
141
expect(idCallOrder.length).toBe(3);
191
- expect(idCallOrder[0]).toBe(CHILD);
192
- expect(idCallOrder[1]).toBe(PARENT);
193
- expect(idCallOrder[2]).toBe(GRANDPARENT);
142
+ expect(idCallOrder[0]).toBe(getInternal(CHILD));
143
+ expect(idCallOrder[1]).toBe(getInternal(PARENT));
144
+ expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT));
145
});
146
147
it('should continue bubbling if an error is thrown', () => {
197
- putListener(
198
- CHILD,
148
+ EventPluginHub.putListener(
149
+ getInternal(CHILD),
150
ON_CLICK_KEY,
200
- recordID.bind(null, CHILD)
151
+ recordID.bind(null, getInternal(CHILD))
152
);
202
- putListener(
203
- PARENT,
153
+ EventPluginHub.putListener(
154
+ getInternal(PARENT),
155
ON_CLICK_KEY,
156
function() {
206
- recordID(PARENT);
157
+ recordID(getInternal(PARENT));
158
throw new Error('Handler interrupted');
159
}
160
);
210
- putListener(
211
- GRANDPARENT,
161
+ EventPluginHub.putListener(
162
+ getInternal(GRANDPARENT),
163
ON_CLICK_KEY,
213
- recordID.bind(null, GRANDPARENT)
164
+ recordID.bind(null, getInternal(GRANDPARENT))
165
);
166
expect(function() {
167
ReactTestUtils.Simulate.click(CHILD);
168
}).toThrow();
169
expect(idCallOrder.length).toBe(3);
219
- expect(idCallOrder[0]).toBe(CHILD);
220
- expect(idCallOrder[1]).toBe(PARENT);
221
- expect(idCallOrder[2]).toBe(GRANDPARENT);
170
+ expect(idCallOrder[0]).toBe(getInternal(CHILD));
171
+ expect(idCallOrder[1]).toBe(getInternal(PARENT));
172
+ expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT));
173
});
174
175
it('should set currentTarget', () => {
225
- putListener(
226
- CHILD,
176
+ EventPluginHub.putListener(
177
+ getInternal(CHILD),
178
ON_CLICK_KEY,
179
function(event) {
229
- recordID(CHILD);
180
+ recordID(getInternal(CHILD));
181
expect(event.currentTarget).toBe(CHILD);
182
}
183
);
233
- putListener(
234
- PARENT,
184
+ EventPluginHub.putListener(
185
+ getInternal(PARENT),
186
ON_CLICK_KEY,
187
function(event) {
237
- recordID(PARENT);
188
+ recordID(getInternal(PARENT));
189
expect(event.currentTarget).toBe(PARENT);
190
}
191
);
241
- putListener(
242
- GRANDPARENT,
192
+ EventPluginHub.putListener(
193
+ getInternal(GRANDPARENT),
194
ON_CLICK_KEY,
195
function(event) {
245
- recordID(GRANDPARENT);
196
+ recordID(getInternal(GRANDPARENT));
197
expect(event.currentTarget).toBe(GRANDPARENT);
198
}
199
);
200
ReactTestUtils.Simulate.click(CHILD);
201
expect(idCallOrder.length).toBe(3);
251
- expect(idCallOrder[0]).toBe(CHILD);
252
- expect(idCallOrder[1]).toBe(PARENT);
253
- expect(idCallOrder[2]).toBe(GRANDPARENT);
202
+ expect(idCallOrder[0]).toBe(getInternal(CHILD));
203
+ expect(idCallOrder[1]).toBe(getInternal(PARENT));
204
+ expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT));
205
});
206
207
it('should support stopPropagation()', () => {
257
- putListener(
258
- CHILD,
208
+ EventPluginHub.putListener(
209
+ getInternal(CHILD),
210
ON_CLICK_KEY,
260
- recordID.bind(null, CHILD)
211
+ recordID.bind(null, getInternal(CHILD))
212
);
262
- putListener(
263
- PARENT,
213
+ EventPluginHub.putListener(
214
+ getInternal(PARENT),
215
ON_CLICK_KEY,
265
- recordIDAndStopPropagation.bind(null, PARENT)
216
+ recordIDAndStopPropagation.bind(null, getInternal(PARENT))
217
);
267
- putListener(
268
- GRANDPARENT,
218
+ EventPluginHub.putListener(
219
+ getInternal(GRANDPARENT),
220
ON_CLICK_KEY,
270
- recordID.bind(null, GRANDPARENT)
221
+ recordID.bind(null, getInternal(GRANDPARENT))
222
);
223
ReactTestUtils.Simulate.click(CHILD);
224
expect(idCallOrder.length).toBe(2);
274
- expect(idCallOrder[0]).toBe(CHILD);
275
- expect(idCallOrder[1]).toBe(PARENT);
225
+ expect(idCallOrder[0]).toBe(getInternal(CHILD));
226
+ expect(idCallOrder[1]).toBe(getInternal(PARENT));
227
});
228
229
it('should stop after first dispatch if stopPropagation', () => {
279
- putListener(
280
- CHILD,
230
+ EventPluginHub.putListener(
231
+ getInternal(CHILD),
232
ON_CLICK_KEY,
282
- recordIDAndStopPropagation.bind(null, CHILD)
233
+ recordIDAndStopPropagation.bind(null, getInternal(CHILD))
234
);
284
- putListener(
285
- PARENT,
235
+ EventPluginHub.putListener(
236
+ getInternal(PARENT),
237
ON_CLICK_KEY,
287
- recordID.bind(null, PARENT)
238
+ recordID.bind(null, getInternal(PARENT))
239
);
289
- putListener(
290
- GRANDPARENT,
240
+ EventPluginHub.putListener(
241
+ getInternal(GRANDPARENT),
242
ON_CLICK_KEY,
292
- recordID.bind(null, GRANDPARENT)
243
+ recordID.bind(null, getInternal(GRANDPARENT))
244
);
245
ReactTestUtils.Simulate.click(CHILD);
246
expect(idCallOrder.length).toBe(1);
296
- expect(idCallOrder[0]).toBe(CHILD);
247
+ expect(idCallOrder[0]).toBe(getInternal(CHILD));
248
});
249
250
it('should not stopPropagation if false is returned', () => {
300
- putListener(
301
- CHILD,
251
+ EventPluginHub.putListener(
252
+ getInternal(CHILD),
253
ON_CLICK_KEY,
303
- recordIDAndReturnFalse.bind(null, CHILD)
254
+ recordIDAndReturnFalse.bind(null, getInternal(CHILD))
255
);
305
- putListener(
306
- PARENT,
256
+ EventPluginHub.putListener(
257
+ getInternal(PARENT),
258
ON_CLICK_KEY,
308
- recordID.bind(null, PARENT)
259
+ recordID.bind(null, getInternal(PARENT))
260
);
310
- putListener(
311
- GRANDPARENT,
261
+ EventPluginHub.putListener(
262
+ getInternal(GRANDPARENT),
263
ON_CLICK_KEY,
313
- recordID.bind(null, GRANDPARENT)
264
+ recordID.bind(null, getInternal(GRANDPARENT))
265
);
266
spyOn(console, 'error');
267
ReactTestUtils.Simulate.click(CHILD);
268
expect(idCallOrder.length).toBe(3);
318
- expect(idCallOrder[0]).toBe(CHILD);
319
- expect(idCallOrder[1]).toBe(PARENT);
320
- expect(idCallOrder[2]).toBe(GRANDPARENT);
269
+ expect(idCallOrder[0]).toBe(getInternal(CHILD));
270
+ expect(idCallOrder[1]).toBe(getInternal(PARENT));
271
+ expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT));
272
expect(console.error.calls.count()).toEqual(0);
273
});
274
@@ -333,15 +284,15 @@ describe('ReactBrowserEventEmitter', () => {
284
it('should invoke handlers that were removed while bubbling', () => {
285
var handleParentClick = jest.fn();
286
var handleChildClick = function(event) {
336
- deleteAllListeners(PARENT);
287
+ EventPluginHub.deleteAllListeners(getInternal(PARENT));
288
};
338
- putListener(
339
- CHILD,
289
+ EventPluginHub.putListener(
290
+ getInternal(CHILD),
291
ON_CLICK_KEY,
292
handleChildClick
293
);
343
- putListener(
344
- PARENT,
294
+ EventPluginHub.putListener(
295
+ getInternal(PARENT),
296
ON_CLICK_KEY,
297
handleParentClick
298
);
@@ -352,14 +303,14 @@ describe('ReactBrowserEventEmitter', () => {
303
it('should not invoke newly inserted handlers while bubbling', () => {
304
var handleParentClick = jest.fn();
305
var handleChildClick = function(event) {
355
- putListener(
356
- PARENT,
306
+ EventPluginHub.putListener(
307
+ getInternal(PARENT),
308
ON_CLICK_KEY,
309
handleParentClick
310
);
311
};
361
- putListener(
362
- CHILD,
312
+ EventPluginHub.putListener(
313
+ getInternal(CHILD),
314
ON_CLICK_KEY,
315
handleChildClick
316
);
@@ -368,21 +319,21 @@ describe('ReactBrowserEventEmitter', () => {
319
});
320
321
it('should have mouse enter simulated by test utils', () => {
371
- putListener(
372
- CHILD,
322
+ EventPluginHub.putListener(
323
+ getInternal(CHILD),
324
ON_MOUSE_ENTER_KEY,
374
- recordID.bind(null, CHILD)
325
+ recordID.bind(null, getInternal(CHILD))
326
);
327
ReactTestUtils.Simulate.mouseEnter(CHILD);
328
expect(idCallOrder.length).toBe(1);
378
- expect(idCallOrder[0]).toBe(CHILD);
329
+ expect(idCallOrder[0]).toBe(getInternal(CHILD));
330
});
331
332
it('should infer onTouchTap from a touchStart/End', () => {
382
- putListener(
383
- CHILD,
333
+ EventPluginHub.putListener(
334
+ getInternal(CHILD),
335
ON_TOUCH_TAP_KEY,
385
- recordID.bind(null, CHILD)
336
+ recordID.bind(null, getInternal(CHILD))
337
);
338
ReactTestUtils.SimulateNative.touchStart(
339
CHILD,
@@ -393,14 +344,14 @@ describe('ReactBrowserEventEmitter', () => {
344
ReactTestUtils.nativeTouchData(0, 0)
345
);
346
expect(idCallOrder.length).toBe(1);
396
- expect(idCallOrder[0]).toBe(CHILD);
347
+ expect(idCallOrder[0]).toBe(getInternal(CHILD));
348
});
349
350
it('should infer onTouchTap from when dragging below threshold', () => {
400
- putListener(
401
- CHILD,
351
+ EventPluginHub.putListener(
352
+ getInternal(CHILD),
353
ON_TOUCH_TAP_KEY,
403
- recordID.bind(null, CHILD)
354
+ recordID.bind(null, getInternal(CHILD))
355
);
356
ReactTestUtils.SimulateNative.touchStart(
357
CHILD,
@@ -411,14 +362,14 @@ describe('ReactBrowserEventEmitter', () => {
362
ReactTestUtils.nativeTouchData(0, tapMoveThreshold - 1)
363
);
364
expect(idCallOrder.length).toBe(1);
414
- expect(idCallOrder[0]).toBe(CHILD);
365
+ expect(idCallOrder[0]).toBe(getInternal(CHILD));
366
});
367
368
it('should not onTouchTap from when dragging beyond threshold', () => {
418
- putListener(
419
- CHILD,
369
+ EventPluginHub.putListener(
370
+ getInternal(CHILD),
371
ON_TOUCH_TAP_KEY,
421
- recordID.bind(null, CHILD)
372
+ recordID.bind(null, getInternal(CHILD))
373
);
374
ReactTestUtils.SimulateNative.touchStart(
375
CHILD,
@@ -472,20 +423,20 @@ describe('ReactBrowserEventEmitter', () => {
423
});
424
425
it('should bubble onTouchTap', () => {
475
- putListener(
476
- CHILD,
426
+ EventPluginHub.putListener(
427
+ getInternal(CHILD),
428
ON_TOUCH_TAP_KEY,
478
- recordID.bind(null, CHILD)
429
+ recordID.bind(null, getInternal(CHILD))
430
);
480
- putListener(
481
- PARENT,
431
+ EventPluginHub.putListener(
432
+ getInternal(PARENT),
433
ON_TOUCH_TAP_KEY,
483
- recordID.bind(null, PARENT)
434
+ recordID.bind(null, getInternal(PARENT))
435
);
485
- putListener(
486
- GRANDPARENT,
436
+ EventPluginHub.putListener(
437
+ getInternal(GRANDPARENT),
438
ON_TOUCH_TAP_KEY,
488
- recordID.bind(null, GRANDPARENT)
439
+ recordID.bind(null, getInternal(GRANDPARENT))
440
);
441
ReactTestUtils.SimulateNative.touchStart(
442
CHILD,
@@ -496,9 +447,9 @@ describe('ReactBrowserEventEmitter', () => {
447
ReactTestUtils.nativeTouchData(0, 0)
448
);
449
expect(idCallOrder.length).toBe(3);
499
- expect(idCallOrder[0]).toBe(CHILD);
500
- expect(idCallOrder[1]).toBe(PARENT);
501
- expect(idCallOrder[2]).toBe(GRANDPARENT);
450
+ expect(idCallOrder[0]).toBe(getInternal(CHILD));
451
+ expect(idCallOrder[1]).toBe(getInternal(PARENT));
452
+ expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT));
453
});
454
455
it('should not crash ensureScrollValueMonitoring when createEvent returns null', () => {
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
+80
@@ -972,6 +972,61 @@ describe('ReactDOMComponent', () => {
972
expect(tracker.getValue()).toEqual('foo');
973
});
974
975
+ it('should execute custom event plugin listening behavior', () => {
976
+ var SimpleEventPlugin = require('SimpleEventPlugin');
977
+
978
+ SimpleEventPlugin.didPutListener = jest.fn();
979
+ SimpleEventPlugin.willDeleteListener = jest.fn();
980
+
981
+ var container = document.createElement('div');
982
+ ReactDOM.render(
983
+ <div onClick={() => true} />,
984
+ container
985
+ );
986
+
987
+ expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(1);
988
+
989
+ ReactDOM.unmountComponentAtNode(container);
990
+
991
+ expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
992
+ });
993
+
994
+ it('should handle null and missing properly with event hooks', () => {
995
+ var SimpleEventPlugin = require('SimpleEventPlugin');
996
+
997
+ SimpleEventPlugin.didPutListener = jest.fn();
998
+ SimpleEventPlugin.willDeleteListener = jest.fn();
999
+ var container = document.createElement('div');
1000
+
1001
+ ReactDOM.render(<div onClick={false} />, container);
1002
+ expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(0);
1003
+ expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
1004
+
1005
+ ReactDOM.render(<div onClick={null} />, container);
1006
+ expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(0);
1007
+ expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
1008
+
1009
+ ReactDOM.render(<div onClick={() => 'apple'} />, container);
1010
+ expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(1);
1011
+ expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
1012
+
1013
+ ReactDOM.render(<div onClick={() => 'banana'} />, container);
1014
+ expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
1015
+ expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
1016
+
1017
+ ReactDOM.render(<div onClick={null} />, container);
1018
+ expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
1019
+ expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
1020
+
1021
+ ReactDOM.render(<div />, container);
1022
+ expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
1023
+ expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
1024
+
1025
+ ReactDOM.unmountComponentAtNode(container);
1026
+ expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
1027
+ expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
1028
+ });
1029
+
1030
it('should warn for children on void elements', () => {
1031
class X extends React.Component {
1032
render() {
@@ -1102,6 +1157,31 @@ describe('ReactDOMComponent', () => {
1157
});
1158
1159
describe('unmountComponent', () => {
1160
+ it('should clean up listeners', () => {
1161
+ var EventPluginHub = require('EventPluginHub');
1162
+ var ReactDOMComponentTree = require('ReactDOMComponentTree');
1163
+
1164
+ var container = document.createElement('div');
1165
+ document.body.appendChild(container);
1166
+
1167
+ var callback = function() {};
1168
+ var instance = <div onClick={callback} />;
1169
+ instance = ReactDOM.render(instance, container);
1170
+
1171
+ var rootNode = ReactDOM.findDOMNode(instance);
1172
+ var inst = ReactDOMComponentTree.getInstanceFromNode(rootNode);
1173
+ expect(
1174
+ EventPluginHub.getListener(inst, 'onClick')
1175
+ ).toBe(callback);
1176
+ expect(rootNode).toBe(ReactDOM.findDOMNode(instance));
1177
+
1178
+ ReactDOM.unmountComponentAtNode(container);
1179
+
1180
+ expect(
1181
+ EventPluginHub.getListener(inst, 'onClick')
1182
+ ).toBe(undefined);
1183
+ });
1184
+
1185
it('should clean up input value tracking', () => {
1186
var container = document.createElement('div');
1187
var node = ReactDOM.render(<input type="text" defaultValue="foo"/>, container);
src/renderers/dom/stack/client/ReactDOMComponent.js
+46
-3
@@ -19,6 +19,7 @@ var DOMLazyTree = require('DOMLazyTree');
19
var DOMNamespaces = require('DOMNamespaces');
20
var DOMProperty = require('DOMProperty');
21
var DOMPropertyOperations = require('DOMPropertyOperations');
22
+var EventPluginHub = require('EventPluginHub');
23
var EventPluginRegistry = require('EventPluginRegistry');
24
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
25
var ReactDOMComponentFlags = require('ReactDOMComponentFlags');
@@ -42,6 +43,7 @@ var warning = require('warning');
43
var didWarnShadyDOM = false;
44
45
var Flags = ReactDOMComponentFlags;
46
+var deleteListener = EventPluginHub.deleteListener;
47
var getNode = ReactDOMComponentTree.getNodeFromInstance;
48
var listenTo = ReactBrowserEventEmitter.listenTo;
49
var registrationNameModules = EventPluginRegistry.registrationNameModules;
@@ -203,6 +205,30 @@ function assertValidProps(component, props) {
205
);
206
}
207
208
+function enqueuePutListener(inst, registrationName, listener, transaction) {
209
+ if (transaction instanceof ReactServerRenderingTransaction) {
210
+ return;
211
+ }
212
+ if (__DEV__) {
213
+ // IE8 has no API for event capturing and the `onScroll` event doesn't
214
+ // bubble.
215
+ warning(
216
+ registrationName !== 'onScroll' || isEventSupported('scroll', true),
217
+ 'This browser doesn\'t support the `onScroll` event'
218
+ );
219
+ }
220
+ var containerInfo = inst._hostContainerInfo;
221
+ var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
222
+ var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
223
+ listenTo(registrationName, doc);
224
+ transaction.getReactMountReady().enqueue(putListener, {
225
+ inst: inst,
226
+ registrationName: registrationName,
227
+ listener: listener,
228
+ });
229
+}
230
+
231
+// TODO: This is coming from future #8192. Dedupe this and enqueuePutListener.
232
function ensureListeningTo(inst, registrationName, transaction) {
233
if (transaction instanceof ReactServerRenderingTransaction) {
234
return;
@@ -221,6 +247,15 @@ function ensureListeningTo(inst, registrationName, transaction) {
247
listenTo(registrationName, doc);
248
}
249
250
+function putListener() {
251
+ var listenerToPut = this;
252
+ EventPluginHub.putListener(
253
+ listenerToPut.inst,
254
+ listenerToPut.registrationName,
255
+ listenerToPut.listener
256
+ );
257
+}
258
+
259
function inputPostMount() {
260
var inst = this;
261
ReactDOMInput.postMountWrapper(inst);
@@ -757,7 +792,7 @@ ReactDOMComponent.Mixin = {
792
}
793
if (registrationNameModules.hasOwnProperty(propKey)) {
794
if (propValue) {
760
- ensureListeningTo(this, propKey, transaction);
795
+ enqueuePutListener(this, propKey, propValue, transaction);
796
}
797
} else {
798
if (propKey === STYLE) {
@@ -1012,7 +1047,12 @@ ReactDOMComponent.Mixin = {
1047
}
1048
this._previousStyleCopy = null;
1049
} else if (registrationNameModules.hasOwnProperty(propKey)) {
1015
- // Do nothing for event names.
1050
+ if (lastProps[propKey]) {
1051
+ // Only call deleteListener if there was a listener previously or
1052
+ // else willDeleteListener gets called when there wasn't actually a
1053
+ // listener (e.g., onClick={null})
1054
+ deleteListener(this, propKey);
1055
+ }
1056
} else if (isCustomComponent(this._tag, lastProps)) {
1057
if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
1058
DOMPropertyOperations.deleteValueForAttribute(
@@ -1073,7 +1113,9 @@ ReactDOMComponent.Mixin = {
1113
}
1114
} else if (registrationNameModules.hasOwnProperty(propKey)) {
1115
if (nextProp) {
1076
- ensureListeningTo(this, propKey, transaction);
1116
+ enqueuePutListener(this, propKey, nextProp, transaction);
1117
+ } else if (lastProp) {
1118
+ deleteListener(this, propKey);
1119
}
1120
} else if (isCustomComponentTag) {
1121
if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
@@ -1222,6 +1264,7 @@ ReactDOMComponent.Mixin = {
1264
1265
this.unmountChildren(safely, skipLifecycle);
1266
ReactDOMComponentTree.uncacheNode(this);
1267
+ EventPluginHub.deleteAllListeners(this);
1268
this._rootNodeID = 0;
1269
this._domID = 0;
1270
this._wrapperState = null;
src/renderers/dom/stack/server/__tests__/ReactServerRendering-test.js
+19
-1
@@ -85,7 +85,15 @@ describe('ReactServerRendering', () => {
85
expect(response).toBe('<!-- react-empty: 1 -->');
86
});
87
88
- // TODO: Test that listeners are not registered onto any document/container.
88
+ it('should not register event listeners', () => {
89
+ var EventPluginHub = require('EventPluginHub');
90
+ var cb = jest.fn();
91
+
92
+ ReactServerRendering.renderToString(
93
+ <span onClick={cb}>hello world</span>
94
+ );
95
+ expect(EventPluginHub.__getListenerBank()).toEqual({});
96
+ });
97
98
it('should render composite components', () => {
99
class Parent extends React.Component {
@@ -314,6 +322,16 @@ describe('ReactServerRendering', () => {
322
expect(response).toBe('<span>hello world</span>');
323
});
324
325
+ it('should not register event listeners', () => {
326
+ var EventPluginHub = require('EventPluginHub');
327
+ var cb = jest.fn();
328
+
329
+ ReactServerRendering.renderToStaticMarkup(
330
+ <span onClick={cb}>hello world</span>
331
+ );
332
+ expect(EventPluginHub.__getListenerBank()).toEqual({});
333
+ });
334
+
335
it('should only execute certain lifecycle methods', () => {
336
function runTest() {
337
var lifecycle = [];
src/renderers/native/ReactNativeBaseComponent.js
+43
@@ -14,12 +14,18 @@
14
var NativeMethodsMixin = require('NativeMethodsMixin');
15
var ReactNativeAttributePayload = require('ReactNativeAttributePayload');
16
var ReactNativeComponentTree = require('ReactNativeComponentTree');
17
+var ReactNativeEventEmitter = require('ReactNativeEventEmitter');
18
var ReactNativeTagHandles = require('ReactNativeTagHandles');
19
var ReactMultiChild = require('ReactMultiChild');
20
var UIManager = require('UIManager');
21
22
var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev');
23
24
+var registrationNames = ReactNativeEventEmitter.registrationNames;
25
+var putListener = ReactNativeEventEmitter.putListener;
26
+var deleteListener = ReactNativeEventEmitter.deleteListener;
27
+var deleteAllListeners = ReactNativeEventEmitter.deleteAllListeners;
28
+
29
type ReactNativeBaseComponentViewConfig = {
30
validAttributes: Object;
31
uiViewClassName: string;
@@ -51,6 +57,7 @@ ReactNativeBaseComponent.Mixin = {
57
58
unmountComponent: function(safely, skipLifecycle) {
59
ReactNativeComponentTree.uncacheNode(this);
60
+ deleteAllListeners(this);
61
this.unmountChildren(safely, skipLifecycle);
62
this._rootNodeID = 0;
63
},
@@ -116,9 +123,44 @@ ReactNativeBaseComponent.Mixin = {
123
);
124
}
125
126
+ this._reconcileListenersUponUpdate(
127
+ prevElement.props,
128
+ nextElement.props
129
+ );
130
this.updateChildren(nextElement.props.children, transaction, context);
131
},
132
133
+ /**
134
+ * @param {object} initialProps Native component props.
135
+ */
136
+ _registerListenersUponCreation: function(initialProps) {
137
+ for (var key in initialProps) {
138
+ // NOTE: The check for `!props[key]`, is only possible because this method
139
+ // registers listeners the *first* time a component is created.
140
+ if (registrationNames[key] && initialProps[key]) {
141
+ var listener = initialProps[key];
142
+ putListener(this, key, listener);
143
+ }
144
+ }
145
+ },
146
+
147
+ /**
148
+ * Reconciles event listeners, adding or removing if necessary.
149
+ * @param {object} prevProps Native component props including events.
150
+ * @param {object} nextProps Next native component props including events.
151
+ */
152
+ _reconcileListenersUponUpdate: function(prevProps, nextProps) {
153
+ for (var key in nextProps) {
154
+ if (registrationNames[key] && (nextProps[key] !== prevProps[key])) {
155
+ if (nextProps[key]) {
156
+ putListener(this, key, nextProps[key]);
157
+ } else {
158
+ deleteListener(this, key);
159
+ }
160
+ }
161
+ }
162
+ },
163
+
164
/**
165
* Currently this still uses IDs for reconciliation so this can return null.
166
*
@@ -165,6 +207,7 @@ ReactNativeBaseComponent.Mixin = {
207
208
ReactNativeComponentTree.precacheNode(this, tag);
209
210
+ this._registerListenersUponCreation(this._currentElement.props);
211
this.initializeChildren(
212
this._currentElement.props.children,
213
tag,
src/renderers/native/ReactNativeEventEmitter.js
+15
@@ -78,14 +78,29 @@ var removeTouchesAtIndices = function(
78
return rippedOut;
79
};
80
81
+/**
82
+ * `ReactNativeEventEmitter` is used to attach top-level event listeners. For example:
83
+ *
84
+ * ReactNativeEventEmitter.putListener('myID', 'onClick', myFunction);
85
+ *
86
+ * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
87
+ *
88
+ * @internal
89
+ */
90
var ReactNativeEventEmitter = {
91
92
...ReactEventEmitterMixin,
93
94
registrationNames: EventPluginRegistry.registrationNameModules,
95
96
+ putListener: EventPluginHub.putListener,
97
+
98
getListener: EventPluginHub.getListener,
99
100
+ deleteListener: EventPluginHub.deleteListener,
101
+
102
+ deleteAllListeners: EventPluginHub.deleteAllListeners,
103
+
104
/**
105
* Internal version of `receiveEvent` in terms of normalized (non-tag)
106
* `rootNodeID`.
src/renderers/shared/shared/event/EventPluginHub.js
+97
-9
@@ -19,6 +19,11 @@ var accumulateInto = require('accumulateInto');
19
var forEachAccumulated = require('forEachAccumulated');
20
var invariant = require('invariant');
21
22
+/**
23
+ * Internal store for event listeners
24
+ */
25
+var listenerBank = {};
26
+
27
/**
28
* Internal queue of events that have accumulated their dispatches and are
29
* waiting to have their dispatches executed.
@@ -48,6 +53,12 @@ var executeDispatchesAndReleaseTopLevel = function(e) {
53
return executeDispatchesAndRelease(e, false);
54
};
55
56
+var getDictionaryKey = function(inst) {
57
+ // Prevents V8 performance issue:
58
+ // https://github.com/facebook/react/pull/7232
59
+ return '.' + inst._rootNodeID;
60
+};
61
+
62
/**
63
* This is a unified interface for event plugins to be installed and configured.
64
*
@@ -90,22 +101,88 @@ var EventPluginHub = {
101
102
},
103
104
+ /**
105
+ * Stores `listener` at `listenerBank[registrationName][key]`. Is idempotent.
106
+ *
107
+ * @param {object} inst The instance, which is the source of events.
108
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
109
+ * @param {function} listener The callback to store.
110
+ */
111
+ putListener: function(inst, registrationName, listener) {
112
+ invariant(
113
+ typeof listener === 'function',
114
+ 'Expected %s listener to be a function, instead got type %s',
115
+ registrationName, typeof listener
116
+ );
117
+
118
+ var key = getDictionaryKey(inst);
119
+ var bankForRegistrationName =
120
+ listenerBank[registrationName] || (listenerBank[registrationName] = {});
121
+ bankForRegistrationName[key] = listener;
122
+
123
+ var PluginModule =
124
+ EventPluginRegistry.registrationNameModules[registrationName];
125
+ if (PluginModule && PluginModule.didPutListener) {
126
+ PluginModule.didPutListener(inst, registrationName, listener);
127
+ }
128
+ },
129
+
130
/**
131
* @param {object} inst The instance, which is the source of events.
132
* @param {string} registrationName Name of listener (e.g. `onClick`).
133
* @return {?function} The stored callback.
134
*/
135
getListener: function(inst, registrationName) {
99
- var listener = inst._currentElement.props[registrationName];
100
- if (listener !== undefined) {
101
- invariant(
102
- typeof listener === 'function',
103
- 'Expected %s listener to be a function, instead got type %s',
104
- registrationName,
105
- typeof listener
106
- );
136
+ var bankForRegistrationName = listenerBank[registrationName];
137
+ var key = getDictionaryKey(inst);
138
+ return bankForRegistrationName && bankForRegistrationName[key];
139
+ },
140
+
141
+ /**
142
+ * Deletes a listener from the registration bank.
143
+ *
144
+ * @param {object} inst The instance, which is the source of events.
145
+ * @param {string} registrationName Name of listener (e.g. `onClick`).
146
+ */
147
+ deleteListener: function(inst, registrationName) {
148
+ var PluginModule =
149
+ EventPluginRegistry.registrationNameModules[registrationName];
150
+ if (PluginModule && PluginModule.willDeleteListener) {
151
+ PluginModule.willDeleteListener(inst, registrationName);
152
+ }
153
+
154
+ var bankForRegistrationName = listenerBank[registrationName];
155
+ // TODO: This should never be null -- when is it?
156
+ if (bankForRegistrationName) {
157
+ var key = getDictionaryKey(inst);
158
+ delete bankForRegistrationName[key];
159
+ }
160
+ },
161
+
162
+ /**
163
+ * Deletes all listeners for the DOM element with the supplied ID.
164
+ *
165
+ * @param {object} inst The instance, which is the source of events.
166
+ */
167
+ deleteAllListeners: function(inst) {
168
+ var key = getDictionaryKey(inst);
169
+ for (var registrationName in listenerBank) {
170
+ if (!listenerBank.hasOwnProperty(registrationName)) {
171
+ continue;
172
+ }
173
+
174
+ if (!listenerBank[registrationName][key]) {
175
+ continue;
176
+ }
177
+
178
+ var PluginModule =
179
+ EventPluginRegistry.registrationNameModules[registrationName];
180
+ if (PluginModule && PluginModule.willDeleteListener) {
181
+ PluginModule.willDeleteListener(inst, registrationName);
182
+ }
183
+
184
+ delete listenerBank[registrationName][key];
185
}
108
- return listener;
186
},
187
188
/**
@@ -183,6 +260,17 @@ var EventPluginHub = {
260
ReactErrorUtils.rethrowCaughtError();
261
},
262
263
+ /**
264
+ * These are needed for tests only. Do not use!
265
+ */
266
+ __purge: function() {
267
+ listenerBank = {};
268
+ },
269
+
270
+ __getListenerBank: function() {
271
+ return listenerBank;
272
+ },
273
+
274
};
275
276
module.exports = EventPluginHub;
src/renderers/shared/shared/event/EventPluginRegistry.js
+2
-1
@@ -130,7 +130,8 @@ function publishEventForPlugin(
130
}
131
132
/**
133
- * Publishes a registration name that is used to identify dispatched events.
133
+ * Publishes a registration name that is used to identify dispatched events and
134
+ * can be used with `EventPluginHub.putListener` to register listeners.
135
*
136
* @param {string} registrationName Registration name to add.
137
* @param {object} PluginModule Plugin publishing the event.
src/renderers/shared/shared/event/PluginModuleType.js
+9
@@ -36,5 +36,14 @@ export type PluginModule<NativeEvent> = {
36
nativeTarget: NativeEvent,
37
nativeEventTarget: EventTarget,
38
) => null | ReactSyntheticEvent,
39
+ didPutListener?: (
40
+ inst: ReactInstance,
41
+ registrationName: string,
42
+ listener: () => void,
43
+ ) => void,
44
+ willDeleteListener?: (
45
+ inst: ReactInstance,
46
+ registrationName: string,
47
+ ) => void,
48
tapMoveThreshold?: number,
49
};
src/renderers/shared/shared/event/__tests__/EventPluginHub-test.js
+7
-9
@@ -14,21 +14,19 @@
14
jest.mock('isEventSupported');
15
16
describe('EventPluginHub', () => {
17
- var React;
18
- var ReactTestUtils;
17
+ var EventPluginHub;
18
+ var isEventSupported;
19
20
beforeEach(() => {
21
jest.resetModuleRegistry();
22
- React = require('React');
23
- ReactTestUtils = require('ReactTestUtils');
22
+ EventPluginHub = require('EventPluginHub');
23
+ isEventSupported = require('isEventSupported');
24
+ isEventSupported.mockReturnValueOnce(false);
25
});
26
26
- it('should prevent non-function listeners, at dispatch', () => {
27
- var node = ReactTestUtils.renderIntoDocument(
28
- <div onClick="not a function" />
29
- );
27
+ it('should prevent non-function listeners', () => {
28
expect(function() {
31
- ReactTestUtils.SimulateNative.click(node);
29
+ EventPluginHub.putListener(1, 'onClick', 'not a function');
30
}).toThrowError(
31
'Expected onClick listener to be a function, instead got type string'
32
);
src/renderers/shared/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js
+8
-38
@@ -233,7 +233,7 @@ var registerTestHandlers = function(eventTestConfig, readableIDToID) {
233
}
234
return config.returnVal;
235
}.bind(null, readableID, nodeConfig);
236
- putListener(getInstanceFromNode(id), registrationName, handler);
236
+ EventPluginHub.putListener(getInstanceFromNode(id), registrationName, handler);
237
}
238
};
239
for (var eventName in eventTestConfig) {
@@ -258,6 +258,9 @@ var registerTestHandlers = function(eventTestConfig, readableIDToID) {
258
return runs;
259
};
260
261
+
262
+
263
+
264
var run = function(config, hierarchyConfig, nativeEventConfig) {
265
var max = NA;
266
var searchForMax = function(nodeConfig) {
@@ -306,30 +309,10 @@ var PARENT_HOST_NODE = { };
309
var CHILD_HOST_NODE = { };
310
var CHILD_HOST_NODE2 = { };
311
309
-var GRANDPARENT_INST = {
310
- _hostParent: null,
311
- _rootNodeID: '1',
312
- _hostNode: GRANDPARENT_HOST_NODE,
313
- _currentElement: { props: {} },
314
-};
315
-var PARENT_INST = {
316
- _hostParent: GRANDPARENT_INST,
317
- _rootNodeID: '2',
318
- _hostNode: PARENT_HOST_NODE,
319
- _currentElement: { props: {} },
320
-};
321
-var CHILD_INST = {
322
- _hostParent: PARENT_INST,
323
- _rootNodeID: '3',
324
- _hostNode: CHILD_HOST_NODE,
325
- _currentElement: { props: {} },
326
-};
327
-var CHILD_INST2 = {
328
- _hostParent: PARENT_INST,
329
- _rootNodeID: '4',
330
- _hostNode: CHILD_HOST_NODE2,
331
- _currentElement: { props: {} },
332
-};
312
+var GRANDPARENT_INST = { _hostParent: null, _rootNodeID: '1', _hostNode: GRANDPARENT_HOST_NODE };
313
+var PARENT_INST = { _hostParent: GRANDPARENT_INST, _rootNodeID: '2', _hostNode: PARENT_HOST_NODE };
314
+var CHILD_INST = { _hostParent: PARENT_INST, _rootNodeID: '3', _hostNode: CHILD_HOST_NODE };
315
+var CHILD_INST2 = { _hostParent: PARENT_INST, _rootNodeID: '4', _hostNode: CHILD_HOST_NODE2 };
316
317
GRANDPARENT_HOST_NODE._reactInstance = GRANDPARENT_INST;
318
PARENT_HOST_NODE._reactInstance = PARENT_INST;
@@ -356,14 +339,6 @@ function getNodeFromInstance(inst) {
339
return inst._hostNode;
340
}
341
359
-function putListener(node, registrationName, handler) {
360
- node._currentElement.props[registrationName] = handler;
361
-}
362
-
363
-function deleteAllListeners(node) {
364
- node._currentElement.props = {};
365
-}
366
-
342
describe('ResponderEventPlugin', () => {
343
344
beforeEach(() => {
@@ -373,11 +348,6 @@ describe('ResponderEventPlugin', () => {
348
EventPluginUtils = require('EventPluginUtils');
349
ResponderEventPlugin = require('ResponderEventPlugin');
350
376
- deleteAllListeners(GRANDPARENT_INST);
377
- deleteAllListeners(PARENT_INST);
378
- deleteAllListeners(CHILD_INST);
379
- deleteAllListeners(CHILD_INST2);
380
-
351
EventPluginUtils.injection.injectComponentTree({
352
getInstanceFromNode,
353
getNodeFromInstance,
src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js
+20
@@ -879,6 +879,26 @@ describe('ReactErrorBoundaries', () => {
879
]);
880
});
881
882
+ it('does not register event handlers for unmounted children', () => {
883
+ var EventPluginHub = require('EventPluginHub');
884
+ var container = document.createElement('div');
885
+ EventPluginHub.putListener = jest.fn();
886
+ ReactDOM.render(
887
+ <ErrorBoundary>
888
+ <button onClick={() => {}}>Click me</button>
889
+ <BrokenRender />
890
+ </ErrorBoundary>,
891
+ container
892
+ );
893
+ expect(EventPluginHub.putListener).not.toBeCalled();
894
+
895
+ log.length = 0;
896
+ ReactDOM.unmountComponentAtNode(container);
897
+ expect(log).toEqual([
898
+ 'ErrorBoundary componentWillUnmount',
899
+ ]);
900
+ });
901
+
902
it('does not call componentWillUnmount when aborting initial mount', () => {
903
var container = document.createElement('div');
904
ReactDOM.render(