Skip to content

Commit

Permalink
Warn early for non-functional event listeners (#10453)
Browse files Browse the repository at this point in the history
* Add early warning for non-functional event listeners

* Use functional listeners in ReactDOMComponent test

To avoid triggering the non-functional event listener component

* spy on console.error in non-function EventPluginHub test

This should warn from ReactDOMFiberComponent, but we just ignore it here

* Remove redundant check for listener

* Add expectation for non-functional listener warning in EventPluginHub

* Hoist listener typeof check in hot paths

* Include stack addendum in non-functional listener warning

* Make it pretty

* Remove it.onnly from ReactDOMFiber test

* Fix message

* Update expected message

* Change invariant message to match the new style

* Fix remaining warning
  • Loading branch information
aweary authored and gaearon committed Aug 23, 2017
1 parent 6ab2869 commit 34780da
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 4 deletions.
13 changes: 12 additions & 1 deletion src/renderers/__tests__/EventPluginHub-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,33 @@ jest.mock('isEventSupported');
describe('EventPluginHub', () => {
var React;
var ReactTestUtils;
var ReactDOMFeatureFlags;

beforeEach(() => {
jest.resetModules();
React = require('react');
ReactTestUtils = require('react-dom/test-utils');
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
});

it('should prevent non-function listeners, at dispatch', () => {
spyOn(console, 'error');
var node = ReactTestUtils.renderIntoDocument(
<div onClick="not a function" />,
);
expect(function() {
ReactTestUtils.SimulateNative.click(node);
}).toThrowError(
'Expected onClick listener to be a function, instead got type string',
'Expected `onClick` listener to be a function, instead got a value of `string` type.',
);
if (ReactDOMFeatureFlags.useFiber) {
expectDev(console.error.calls.count()).toBe(1);
expectDev(console.error.calls.argsFor(0)[0]).toContain(
'Expected `onClick` listener to be a function, instead got a value of `string` type.',
);
} else {
expectDev(console.error.calls.count()).toBe(0);
}
});

it('should not prevent null listeners, at dispatch', () => {
Expand Down
20 changes: 20 additions & 0 deletions src/renderers/dom/fiber/ReactDOMFiberComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var setTextContent = require('setTextContent');

if (__DEV__) {
var warning = require('fbjs/lib/warning');
var {getCurrentFiberStackAddendum} = require('ReactDebugCurrentFiber');
var ReactDOMInvalidARIAHook = require('ReactDOMInvalidARIAHook');
var ReactDOMNullInputValuePropHook = require('ReactDOMNullInputValuePropHook');
var ReactDOMUnknownPropertyHook = require('ReactDOMUnknownPropertyHook');
Expand Down Expand Up @@ -118,6 +119,16 @@ if (__DEV__) {
warning(false, 'Extra attributes from the server: %s', names);
};

var warnForInvalidEventListener = function(registrationName, listener) {
warning(
false,
'Expected `%s` listener to be a function, instead got a value of `%s` type.%s',
registrationName,
typeof listener,
getCurrentFiberStackAddendum(),
);
};

var testDocument;
// Parse the HTML and read it back to normalize the HTML string so that it
// can be used for comparison.
Expand Down Expand Up @@ -223,6 +234,9 @@ function setInitialDOMProperties(
// Noop
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
}
} else if (isCustomComponentTag) {
Expand Down Expand Up @@ -695,6 +709,9 @@ var ReactDOMFiberComponent = {
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
// We eagerly listen to this even though we haven't committed yet.
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
}
if (!updatePayload && lastProp !== nextProp) {
Expand Down Expand Up @@ -934,6 +951,9 @@ var ReactDOMFiberComponent = {
}
}
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (__DEV__ && typeof nextProp !== 'function') {

This comment has been minimized.

Copy link
@gaearon

gaearon Aug 24, 2017

Collaborator

Just notice this should probably be inside if (nextProp), no? Otherwise it will "catch" null and undefined which are valid values. I think this is a bug. @aweary What do you think?

Also ideally we should check if (nextProp !== null && typeof nextProp !== 'undefined') rather than just if (nextProp)? Although that's a separate issue.

This comment has been minimized.

Copy link
@aweary

aweary Aug 24, 2017

Author Contributor

Yupp, you're totally right! I'll send a follow-up PR with a fix.

warnForInvalidEventListener(propKey, nextProp);
}
if (nextProp) {
ensureListeningTo(rootContainerElement, propKey);
}
Expand Down
22 changes: 22 additions & 0 deletions src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ var ReactTestUtils = require('react-dom/test-utils');
var PropTypes = require('prop-types');

describe('ReactDOMFiber', () => {
function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
}

var container;
var ReactFeatureFlags;

Expand Down Expand Up @@ -913,6 +917,24 @@ describe('ReactDOMFiber', () => {
]);
});

it('should warn for non-functional event listeners', () => {
spyOn(console, 'error');
class Example extends React.Component {
render() {
return <div onClick="woops" />;
}
}
ReactDOM.render(<Example />, container);
expectDev(console.error.calls.count()).toBe(1);
expectDev(
normalizeCodeLocInfo(console.error.calls.argsFor(0)[0]),
).toContain(
'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' +
' in div (at **)\n' +
' in Example (at **)',
);
});

it('should not update event handlers until commit', () => {
let ops = [];
const handlerA = () => ops.push('A');
Expand Down
4 changes: 2 additions & 2 deletions src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1690,8 +1690,8 @@ describe('ReactDOMComponent', () => {
ReactTestUtils.renderIntoDocument(
<div className="foo1">
<div class="foo2" />
<div onClick="foo3" />
<div onclick="foo4" />
<div onClick={() => {}} />
<div onclick={() => {}} />
<div className="foo5" />
<div className="foo6" />
</div>,
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/shared/shared/event/EventPluginHub.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ var EventPluginHub = {

invariant(
!listener || typeof listener === 'function',
'Expected %s listener to be a function, instead got type %s',
'Expected `%s` listener to be a function, instead got a value of `%s` type.',
registrationName,
typeof listener,
);
Expand Down

0 comments on commit 34780da

Please sign in to comment.