@samitouri / QOS-React-2 / commits / 16cc45156f

Move guards from auto binding to event dispatch

This wraps a proper guard around event dispatching so that errors doesn't interupt other event callbacks. Instead we rethrow the first error after all callbacks have been invoked. For DEV mode we use native event dispatching if available. This has the benefit that caught exceptions show up in the dev tools even without caught exceptions being turned on. Yet, all callbacks are guaranteed to fire.

Sebastian Markbage committed Jun 19, 2015 at 22:31 UTC 16cc45156f65ff7fdda57383759121f59f585e41
6 files changed +82 -48
src/isomorphic/classic/class/ReactClass.js
+1 -5
@@ -13,7 +13,6 @@
13
14 var ReactComponent = require('ReactComponent');
15 var ReactElement = require('ReactElement');
16 -var ReactErrorUtils = require('ReactErrorUtils');
16 var ReactPropTypeLocations = require('ReactPropTypeLocations');
17 var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
18 var ReactNoopUpdateQueue = require('ReactNoopUpdateQueue');
@@ -705,10 +704,7 @@ function bindAutoBindMethods(component) {
704 var method = component.__reactAutoBindMap[autoBindKey];
705 component[autoBindKey] = bindAutoBindMethod(
706 component,
708 - ReactErrorUtils.guard(
709 - method,
710 - component.constructor.displayName + '.' + autoBindKey
711 - )
707 + method
708 );
709 }
710 }
src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js
+28
@@ -161,6 +161,34 @@ describe('ReactBrowserEventEmitter', function() {
161 expect(idCallOrder[2]).toBe(getID(GRANDPARENT));
162 });
163
164 + it('should continue bubbling if an error is thrown', function() {
165 + ReactBrowserEventEmitter.putListener(
166 + getID(CHILD),
167 + ON_CLICK_KEY,
168 + recordID.bind(null, getID(CHILD))
169 + );
170 + ReactBrowserEventEmitter.putListener(
171 + getID(PARENT),
172 + ON_CLICK_KEY,
173 + function() {
174 + recordID(getID(PARENT));
175 + throw new Error('Handler interrupted');
176 + }
177 + );
178 + ReactBrowserEventEmitter.putListener(
179 + getID(GRANDPARENT),
180 + ON_CLICK_KEY,
181 + recordID.bind(null, getID(GRANDPARENT))
182 + );
183 + expect(function() {
184 + ReactTestUtils.Simulate.click(CHILD);
185 + }).toThrow();
186 + expect(idCallOrder.length).toBe(3);
187 + expect(idCallOrder[0]).toBe(getID(CHILD));
188 + expect(idCallOrder[1]).toBe(getID(PARENT));
189 + expect(idCallOrder[2]).toBe(getID(GRANDPARENT));
190 + });
191 +
192 it('should set currentTarget', function() {
193 ReactBrowserEventEmitter.putListener(
194 getID(CHILD),
src/renderers/shared/event/EventPluginHub.js
+3
@@ -13,6 +13,7 @@
13
14 var EventPluginRegistry = require('EventPluginRegistry');
15 var EventPluginUtils = require('EventPluginUtils');
16 +var ReactErrorUtils = require('ReactErrorUtils');
17
18 var accumulateInto = require('accumulateInto');
19 var forEachAccumulated = require('forEachAccumulated');
@@ -282,6 +283,8 @@ var EventPluginHub = {
283 'processEventQueue(): Additional events were enqueued while processing ' +
284 'an event queue. Support for this has not yet been implemented.'
285 );
286 + // This would be a good time to rethrow if any of the event handlers threw.
287 + ReactErrorUtils.rethrowCaughtError();
288 },
289
290 /**
src/renderers/shared/event/EventPluginUtils.js
+4 -1
@@ -12,6 +12,7 @@
12 'use strict';
13
14 var EventConstants = require('EventConstants');
15 +var ReactErrorUtils = require('ReactErrorUtils');
16
17 var invariant = require('invariant');
18 var warning = require('warning');
@@ -108,7 +109,9 @@ function forEachEventDispatch(event, cb) {
109 */
110 function executeDispatch(event, listener, domID) {
111 event.currentTarget = injection.Mount.getNode(domID);
111 - var returnValue = listener(event, domID);
112 + var type = event.type || 'unknown-event';
113 + var returnValue =
114 + ReactErrorUtils.invokeGuardedCallback(type, listener, event, domID);
115 event.currentTarget = null;
116 return returnValue;
117 }
src/renderers/shared/reconciler/__tests__/ReactCompositeComponentError-test.js deleted
-34
@@ -1,34 +0,0 @@
1 -/**
2 - * Copyright 2013-2015, Facebook, Inc.
3 - * All rights reserved.
4 - *
5 - * This source code is licensed under the BSD-style license found in the
6 - * LICENSE file in the root directory of this source tree. An additional grant
7 - * of patent rights can be found in the PATENTS file in the same directory.
8 - *
9 - * @emails react-core
10 - */
11 -
12 -'use strict';
13 -
14 -var React = require('React');
15 -var ReactErrorUtils = require('ReactErrorUtils');
16 -
17 -describe('ReactCompositeComponent-error', function() {
18 -
19 - it('should be passed the component and method name', function() {
20 - spyOn(ReactErrorUtils, 'guard').andCallThrough();
21 - var Component = React.createClass({
22 - someHandler: function() {},
23 - render: function() {
24 - return <div />;
25 - },
26 - });
27 -
28 - void new Component();
29 -
30 - expect(ReactErrorUtils.guard.mostRecentCall.args[1])
31 - .toEqual('Component.someHandler');
32 - });
33 -
34 -});
src/shared/utils/ReactErrorUtils.js
+46 -8
@@ -12,19 +12,57 @@
12
13 'use strict';
14
15 +var caughtError = null;
16 +
17 var ReactErrorUtils = {
18 /**
17 - * Creates a guarded version of a function. This is supposed to make debugging
18 - * of event handlers easier. To aid debugging with the browser's debugger,
19 - * this currently simply returns the original function.
19 + * Call a function while guarding against errors that happens within it.
20 *
21 - * @param {function} func Function to be executed
22 - * @param {string} name The name of the guard
23 - * @return {function}
21 + * @param name (?String) name of the guard to use for logging or debugging
22 + * @param func (Function) function to invoke
23 + * @param a (*) a First argument
24 + * @param b (*) b Second argument
25 + */
26 + invokeGuardedCallback: function(name, func, a, b) {
27 + try {
28 + return func(a, b);
29 + } catch(x) {
30 + if (caughtError === null) {
31 + caughtError = x;
32 + }
33 + return undefined;
34 + }
35 + },
36 +
37 + /**
38 + * During execution of guarded functions we will capture the first error which
39 + * we will rethrow to be handled by the top level error handler.
40 */
25 - guard: function(func, name) {
26 - return func;
41 + rethrowCaughtError: function() {
42 + if (caughtError) {
43 + var error = caughtError;
44 + caughtError = null;
45 + throw error;
46 + }
47 },
48 };
49
50 +if (__DEV__) {
51 + /**
52 + * To help development we can get better devtools integration by simulating a
53 + * real browser event.
54 + */
55 + if (typeof window !== 'undefined' &&
56 + typeof window.dispatchEvent === 'function' &&
57 + typeof Event === 'function') {
58 + var fakeNode = document.createElement('react');
59 + ReactErrorUtils.invokeGuardedCallback = function(name, func, a, b) {
60 + var boundFunc = func.bind(null, a, b);
61 + fakeNode.addEventListener(name, boundFunc, false);
62 + fakeNode.dispatchEvent(new Event(name));
63 + fakeNode.removeEventListener(name, boundFunc, false);
64 + };
65 + }
66 +}
67 +
68 module.exports = ReactErrorUtils;