Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add callback validation to fiber-based renderers #8677

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions scripts/fiber/tests-failing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ src/isomorphic/classic/__tests__/ReactContextValidator-test.js
src/renderers/dom/__tests__/ReactDOMProduction-test.js
* should throw with an error code in production

src/renderers/dom/shared/__tests__/ReactDOM-test.js
* throws in render() if the mount callback is not a function
* throws in render() if the update callback is not a function

src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
* should clean up input value tracking
* should clean up input textarea tracking
Expand Down
2 changes: 2 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,8 @@ src/renderers/dom/shared/__tests__/ReactDOM-test.js
* should overwrite props.children with children argument
* should purge the DOM cache when removing nodes
* allow React.DOM factories to be called without warnings
* throws in render() if the mount callback is not a function
* throws in render() if the update callback is not a function
* preserves focus
* calls focus() on autoFocus elements after they have been mounted to the DOM

Expand Down
12 changes: 6 additions & 6 deletions src/renderers/dom/shared/__tests__/ReactDOM-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,15 @@ describe('ReactDOM', () => {

var myDiv = document.createElement('div');
expect(() => ReactDOM.render(<A />, myDiv, 'no')).toThrowError(
'ReactDOM.render(...): Expected the last optional `callback` argument ' +
'render(...): Expected the last optional `callback` argument ' +
'to be a function. Instead received: string.'
);
expect(() => ReactDOM.render(<A />, myDiv, {})).toThrowError(
'ReactDOM.render(...): Expected the last optional `callback` argument ' +
'render(...): Expected the last optional `callback` argument ' +
'to be a function. Instead received: Object.'
);
expect(() => ReactDOM.render(<A />, myDiv, new Foo())).toThrowError(
'ReactDOM.render(...): Expected the last optional `callback` argument ' +
'render(...): Expected the last optional `callback` argument ' +
'to be a function. Instead received: Foo (keys: a, b).'
);
});
Expand All @@ -164,15 +164,15 @@ describe('ReactDOM', () => {
ReactDOM.render(<A />, myDiv);

expect(() => ReactDOM.render(<A />, myDiv, 'no')).toThrowError(
'ReactDOM.render(...): Expected the last optional `callback` argument ' +
'render(...): Expected the last optional `callback` argument ' +
'to be a function. Instead received: string.'
);
expect(() => ReactDOM.render(<A />, myDiv, {})).toThrowError(
'ReactDOM.render(...): Expected the last optional `callback` argument ' +
'render(...): Expected the last optional `callback` argument ' +
'to be a function. Instead received: Object.'
);
expect(() => ReactDOM.render(<A />, myDiv, new Foo())).toThrowError(
'ReactDOM.render(...): Expected the last optional `callback` argument ' +
'render(...): Expected the last optional `callback` argument ' +
'to be a function. Instead received: Foo (keys: a, b).'
);
});
Expand Down
3 changes: 2 additions & 1 deletion src/renderers/dom/stack/client/ReactMount.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var invariant = require('invariant');
var setInnerHTML = require('setInnerHTML');
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
var warning = require('warning');
var validateCallback = require('validateCallback');

var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
var ROOT_ATTR_NAME = DOMProperty.ROOT_ATTRIBUTE_NAME;
Expand Down Expand Up @@ -432,7 +433,7 @@ var ReactMount = {
},

_renderSubtreeIntoContainer: function(parentComponent, nextElement, container, callback) {
ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');
validateCallback(callback, 'ReactDOM.render');
invariant(
React.isValidElement(nextElement),
'ReactDOM.render(): Invalid component element.%s',
Expand Down
34 changes: 0 additions & 34 deletions src/renderers/shared/fiber/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,6 @@ var invariant = require('invariant');

const isArray = Array.isArray;

function formatUnexpectedArgument(arg) {
var type = typeof arg;
if (type !== 'object') {
return type;
}
var displayName = arg.constructor && arg.constructor.name || type;
var keys = Object.keys(arg);
if (keys.length > 0 && keys.length < 20) {
return `${displayName} (keys: ${keys.join(', ')})`;
}
return displayName;
}

function validateCallback(callback, callerName) {
if (typeof callback !== 'function') {
invariant(
false,
'%s(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callerName,
formatUnexpectedArgument(callback)
);
}
}

module.exports = function(
scheduleUpdate : (fiber : Fiber, priorityLevel : PriorityLevel) => void,
getPriorityContext : () => PriorityLevel,
Expand All @@ -67,27 +42,18 @@ module.exports = function(
const updater = {
isMounted,
enqueueSetState(instance, partialState, callback) {
if (callback) {
validateCallback(callback, 'setState');
}
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext();
addUpdate(fiber, partialState, callback || null, priorityLevel);
scheduleUpdate(fiber, priorityLevel);
},
enqueueReplaceState(instance, state, callback) {
if (callback) {
validateCallback(callback, 'replaceState');
}
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext();
addReplaceUpdate(fiber, state, callback || null, priorityLevel);
scheduleUpdate(fiber, priorityLevel);
},
enqueueForceUpdate(instance, callback) {
if (callback) {
validateCallback(callback, 'forceUpdate');
}
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext();
addForceUpdate(fiber, callback || null, priorityLevel);
Expand Down
28 changes: 9 additions & 19 deletions src/renderers/shared/fiber/ReactFiberUpdateQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const {
TaskPriority,
} = require('ReactPriorityLevel');

const validateCallback = require('validateCallback');
const warning = require('warning');

type PartialState<State, Props> =
Expand Down Expand Up @@ -204,12 +205,14 @@ function findInsertionPosition(queue, update) : Update | null {
// we shouldn't make a copy.
//
// If the update is cloned, it returns the cloned update.
function insertUpdate(fiber : Fiber, update : Update, methodName : ?string) : Update | null {
function insertUpdate(fiber : Fiber, update : Update, methodName : string) : Update | null {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since methodName is now always a string, we can remove the typeof check on line 215.

validateCallback(update.callback, methodName);

const queue1 = ensureUpdateQueue(fiber);
const queue2 = fiber.alternate ? ensureUpdateQueue(fiber.alternate) : null;

// Warn if an update is scheduled from inside an updater function.
if (__DEV__ && typeof methodName === 'string') {
if (__DEV__) {
if (queue1.isProcessing || (queue2 && queue2.isProcessing)) {
if (methodName === 'setState') {
warning(
Expand Down Expand Up @@ -287,11 +290,7 @@ function addUpdate(
isTopLevelUnmount: false,
next: null,
};
if (__DEV__) {
insertUpdate(fiber, update, 'setState');
} else {
insertUpdate(fiber, update);
}
insertUpdate(fiber, update, 'setState');
}
exports.addUpdate = addUpdate;

Expand All @@ -310,12 +309,7 @@ function addReplaceUpdate(
isTopLevelUnmount: false,
next: null,
};

if (__DEV__) {
insertUpdate(fiber, update, 'replaceState');
} else {
insertUpdate(fiber, update);
}
insertUpdate(fiber, update, 'replaceState');
}
exports.addReplaceUpdate = addReplaceUpdate;

Expand All @@ -333,11 +327,7 @@ function addForceUpdate(
isTopLevelUnmount: false,
next: null,
};
if (__DEV__) {
insertUpdate(fiber, update, 'forceUpdate');
} else {
insertUpdate(fiber, update);
}
insertUpdate(fiber, update, 'forceUpdate');
}
exports.addForceUpdate = addForceUpdate;

Expand Down Expand Up @@ -366,7 +356,7 @@ function addTopLevelUpdate(
isTopLevelUnmount,
next: null,
};
const update2 = insertUpdate(fiber, update);
const update2 = insertUpdate(fiber, update, 'render');

if (isTopLevelUnmount) {
// Drop all updates that are lower-priority, so that the tree is not
Expand Down
31 changes: 4 additions & 27 deletions src/renderers/shared/stack/reconciler/ReactUpdateQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,13 @@ var ReactInstanceMap = require('ReactInstanceMap');
var ReactInstrumentation = require('ReactInstrumentation');
var ReactUpdates = require('ReactUpdates');

var invariant = require('invariant');
var warning = require('warning');
var validateCallback = require('validateCallback');

function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}

function formatUnexpectedArgument(arg) {
var type = typeof arg;
if (type !== 'object') {
return type;
}
var displayName = arg.constructor && arg.constructor.name || type;
var keys = Object.keys(arg);
if (keys.length > 0 && keys.length < 20) {
return `${displayName} (keys: ${keys.join(', ')})`;
}
return displayName;
}

function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
var internalInstance = ReactInstanceMap.get(publicInstance);
if (!internalInstance) {
Expand Down Expand Up @@ -147,7 +134,7 @@ var ReactUpdateQueue = {
}

if (callback) {
ReactUpdateQueue.validateCallback(callback, callerName);
validateCallback(callback, callerName);
if (internalInstance._pendingCallbacks) {
internalInstance._pendingCallbacks.push(callback);
} else {
Expand Down Expand Up @@ -187,7 +174,7 @@ var ReactUpdateQueue = {
internalInstance._pendingReplaceState = true;

if (callback) {
ReactUpdateQueue.validateCallback(callback, callerName);
validateCallback(callback, callerName);
if (internalInstance._pendingCallbacks) {
internalInstance._pendingCallbacks.push(callback);
} else {
Expand Down Expand Up @@ -235,7 +222,7 @@ var ReactUpdateQueue = {
queue.push(partialState);

if (callback) {
ReactUpdateQueue.validateCallback(callback, callerName);
validateCallback(callback, callerName);
if (internalInstance._pendingCallbacks) {
internalInstance._pendingCallbacks.push(callback);
} else {
Expand All @@ -253,16 +240,6 @@ var ReactUpdateQueue = {
enqueueUpdate(internalInstance);
},

validateCallback: function(callback, callerName) {
invariant(
!callback || typeof callback === 'function',
'%s(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callerName,
formatUnexpectedArgument(callback)
);
},

};

module.exports = ReactUpdateQueue;
40 changes: 40 additions & 0 deletions src/renderers/shared/utils/validateCallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule validateCallback
* @flow
*/

'use strict';

const invariant = require('invariant');

function formatUnexpectedArgument(arg: any) {
let type = typeof arg;
if (type !== 'object') {
return type;
}
let displayName = arg.constructor && arg.constructor.name || type;
let keys = Object.keys(arg);
if (keys.length > 0 && keys.length < 20) {
return `${displayName} (keys: ${keys.join(', ')})`;
}
return displayName;
}

function validateCallback(callback: ?Function, callerName: string) {
invariant(
!callback || typeof callback === 'function',
'%s(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callerName,
formatUnexpectedArgument(callback)
);
}

module.exports = validateCallback;