-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Conversation
@@ -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: ' + |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Also I don't have a a strong opinion on the top-level API. It is a bit weird that |
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 I thought a bit about reducing the number of parameters required by the external interface by creating a couple of wrapper functions (eg Sorry for the less than helpful feedback 😄 |
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? |
@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); |
There was a problem hiding this comment.
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.
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 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. |
fb8c861
to
a043ebb
Compare
Ok I inverted the dependency between 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 |
error = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, ReactPropTypesSecret); | ||
} catch (ex) { | ||
error = ex; | ||
function formatMessage(message) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah.
There was a problem hiding this comment.
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.
getCurrentFiberOwnerName() |
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
9c5bd00
to
033fa98
Compare
} = require('ReactComponentTreeHook'); | ||
} | ||
|
||
const ReactDebugCurrentFrame = { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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() : ''; |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
033fa98
to
d115f88
Compare
|
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.
d115f88
to
03ed343
Compare
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.