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

API for checking external objects against React prop types #9004

Merged
merged 9 commits into from
Feb 23, 2017

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Feb 14, 2017

Allows users to check arbitrary objects against React prop types without relying on the implementation details of the prop validators themselves. An example of why this is important: some prop validators may throw, while others return an error object. Calling a prop validator manually requires handling both cases. checkPropTypes does this for you.

The default behavior is to skip repeated warnings, like normal prop type checking already does. I added an optional parameter to override that and always warn; not sure if we really need that though.

@acdlite acdlite changed the title [WIP] API for checking external objects against React prop types API for checking external objects against React prop types Feb 15, 2017
@@ -261,7 +261,7 @@ describe('ReactContextValidator', () => {
ReactTestUtils.renderIntoDocument(<Component testContext={{bar: 123}} />);
expectDev(console.error.calls.count()).toBe(1);
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
'Warning: Failed childContext type: ' +
'Warning: Failed child context type: ' +
Copy link
Collaborator

Choose a reason for hiding this comment

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

If this is released in 15.x, it would potentially break CIs on libraries that assert about this warning in tests.

We have some tests like this about prop types in React Redux so I wouldn't be surprised if somebody tested a similar thing about context types.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think this needs to be released until 16. Could be mistaken.

@acdlite
Copy link
Collaborator Author

acdlite commented Feb 15, 2017

Also I don't have a a strong opinion on the top-level API. It is a bit weird that checkPropTypes is the only member of React.PropTypes that is not a prop type. Open to other API suggestions, just chose this one because it was the most straightforward for now.

@bvaughn
Copy link
Contributor

bvaughn commented Feb 15, 2017

It is a bit weird that checkPropTypes is the only member of React.PropTypes that is not a prop type.

Agreed it's a bit weird. Don't feel super strongly about it though.

function checkPropTypes(propTypes, object, location, componentName, warnOnRepeat) {}

Something about the number and ordering of arguments here feels a bit awkward to me from an external use perspective. (Although I haven't found myself wanting to manually validate propTypes before so perhaps I'm just not a good representative for this use case.)

I thought a bit about reducing the number of parameters required by the external interface by creating a couple of wrapper functions (eg checkContextTypes(contextTypes, context, componentName = 'React class')) but this feels awkward too. Maybe it's okay as-is.

Sorry for the less than helpful feedback 😄

@aweary
Copy link
Contributor

aweary commented Feb 15, 2017

Whats the use case for manually checking an object against propTypes? Won't this just encourage and provide an API for users to couple application logic to propType validation, which is supposed to be stripped in production?

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Feb 15, 2017

@aweary It used to be the case that people were doing this anyway. They were sloppy about making it DEV only, so they could rely on it working in production, which is why we've turned all these into noops in production. Since this method doesn't throw or return a value, there's nothing to rely on.

@@ -123,6 +123,12 @@ if (__DEV__) {
};
}

function checkPropTypes(propTypes, object, location, componentName, warnOnRepeat) {
return checkReactTypeSpec(propTypes, object, location, componentName, null, null, warnOnRepeat);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you remove the return here?

Can you confirm that this never returns a value and never throws? That is important because that is what lets us strip it out safely in production.

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Feb 15, 2017

Ideally we should be able to express all our validation logic in terms of this helper. That way we can move this to an external reusable library and just call into it from ReactElementValidator directly. I.e. invert the dependency between checkReactTypeSpec and checkPropTypes. Can we describe checkReactTypeSpec in terms of checkPropTypes instead of vice versa?

What would we need from the API to make it possible?

Perhaps we need to pass a callback into this helper to provide further formatting options? That might mean that we don't need as many arguments passed.

@acdlite
Copy link
Collaborator Author

acdlite commented Feb 16, 2017

Ok I inverted the dependency between checkPropTypes and checkReactTypeSpec.

I wasn't able to cut down the number of arguments much. In order to do that, we'd need to change the signature of the validators themselves, which currently expect componentName and location.

error = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, ReactPropTypesSecret);
} catch (ex) {
error = ex;
function formatMessage(message) {
Copy link
Collaborator

@sebmarkbage sebmarkbage Feb 16, 2017

Choose a reason for hiding this comment

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

This closure is going to be created for every element on the entire page, and every update. That's an awful lot of allocation. Even for DEV.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this could be designed with global context. In Fiber we went with an approach that picked up the current context from a global mutable.

That way we don't have to pass it along as an additional argument everywhere.

If it did it that way this function could just be a global instead of creating a closure.

It seems natural for things like ReactElementValidator anyway since we read the owner from ReactCurrentOwner anyway. We just pass it in a very round about way.

* @param {?boolean} warnOnRepeat Whether or not repeated warnings should be skipped.
* @private
*/
function checkPropTypes(typeSpecs, values, location, componentName, formatMessage, warnOnRepeat) {
Copy link
Collaborator

@sebmarkbage sebmarkbage Feb 16, 2017

Choose a reason for hiding this comment

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

When do we ever want warnOnRepeat to be true? Seems like this doesn't have to be optional and simplifies the API a bit. Tests can be written by resetting the module cache.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Seemed like it might be a useful opt-in, perhaps if a lib's error messages are less specific and don't include the component name. But yeah I'll just get rid of it.

// same error.
loggedTypeFailures[error.message] = true;

var message = formatMessage ? formatMessage(error.message) : error.message;
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems like all we really need format message for is to get the stack. That seems like something very specific that would be a common use case. Maybe we should make it getStack instead?

var stack = getStack ? getStack() : '';
var message = error.message + (stack ? '\n' + stack : '');

That way, if browser devtools let us actually mess around with the stack that console.error produces, or if we can attach custom stacks to new Error, then we have an ability to do that separately.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure I understand the advantage of getStack over formatMessage? Isn't the latter a superset of the former?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not if the error logger can do something special with it in the future.

Imagine if console.error was enhanced to do something like this:

console.error({ message, stack });

Then the message was shown in the console and then the stack expanded when you click the little nub. With links to the source code.

The data structure of Error has a separate message and stack.

Similarly, if you log the warnings to a database, like we do, you likely want to put the stack in a separate column so that you can group on the error messages. If you wanted to do that with a single message, you'd have to write a manual parser that splits them back up. If every user of this API does it slightly differently, it is a pain to find a parser that can figure it out for all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Interesting!

'Failed %s type: %s%s',
location,
error.message,
componentStackInfo
Copy link
Collaborator

Choose a reason for hiding this comment

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

At FB we actually split each of these into a separate column in the database logs. So if we can preserve that, that'd be great.

} = require('ReactComponentTreeHook');
}

const ReactDebugCurrentFrame = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems like this whole thing should be wrapper in __DEV__.

* @param {?Function} getStack Returns the component stack.
* @private
*/
function checkPropTypes(typeSpecs, values, location, componentName, getStack) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The content of this needs to be wrapped in __DEV__ so that it can be filtered out. It won't be DCE since it's exported as a public API.

// same error.
loggedTypeFailures[error.message] = true;

var stack = getStack ? getStack() : '';
Copy link
Collaborator

@sebmarkbage sebmarkbage Feb 22, 2017

Choose a reason for hiding this comment

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

Maybe default could be new Error().stack if you don't provide one? EDIT: I guess in Chrome you get that anyway.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

lgtm but you need to wrap some stuff in __DEV__. Try running the build in master and then against your branch to validate that prod build size hasn't gone up.

You also need to fix the browserify stuff so that CI doesn't fail.

@acdlite
Copy link
Collaborator Author

acdlite commented Feb 22, 2017

   raw     gz Compared to master @ 5f6f3277f5ada837fd6096b8ad330f4914ae6200
     =      = build/react-dom-fiber.min.js
     =      = build/react-dom-server.min.js
     =      = build/react-dom.min.js
   -34    +14 build/react-with-addons.min.js
   -34     +4 build/react.min.js

@acdlite acdlite added this to the 16.0 milestone Feb 22, 2017
ReactPropTypeLocationNames was a map of location identifiers to location
display names, for use in warnings. But in practice, this did nothing
except rename "childContext" to "child context." Not worth it, IMO.

Since we are going to add an API for running prop type checks manually,
we need the ability to support arbitrary prop type locations. So
each place that used to accept a ReactPropTypeLocation now just accepts
a string.
Allows users to check arbitrary objects against React prop types without
relying on the implementation details of the prop validators themselves.
An example of why this is important: some prop validators may throw,
while others return an error object. Calling a prop validator manually
requires handling both cases. `checkPropTypes` does this for you.
Expressing checkReactTypeSpec using the public API frees us to move it
to a separate module in the future.
Don't need this internally; doubt external users will need it either
Gives us flexibility in the future to deal with the stack and the
message separately.
There's a lot of overlap between ReactCurrentOwner,
ReactDebugCurrentFrame, and ReactDebugCurrentFiber that should
be consolidated.
ReactDebugCurrentFrame is shared state.

checkPropTypes should be imported via the main React export,
not imported directly.
@acdlite acdlite merged commit f3c2d9f into facebook:master Feb 23, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants