-
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
Fixed invalid prop types error message to be more specific #11308
Conversation
'the value to a string.%s', | ||
'If you intentionally tried to pass a boolean, pass it as a string ' + | ||
'instead: `%s`="`%s`". If you mean to conditionally pass an ' + | ||
'attribute, use a ternary expression: `%s`={condition ? value : null}.%s', |
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 the value is true
then this is very unlikely to happen because the problematic pattern is condition && value
specifically. I can't think of a way where you'd end up with true
for the same reason. Maybe we should have two separate error messages for true
and false
where this recommendation is only for the false
case?
Maybe also call out that the problematic pattern is condition && value
specifically, so that people know what's wrong and what to look for along with your proposed recommendation.
Also, the recommendation should be to pass undefined
, not null
. It doesn't really matter for DOM components, but if it is a custom component that is the difference between defaultProps working and not working. So it's better to have consistent advice.
I may need some help with the error that occurred in the integration test. I believe it is coming from adding an additional condition in the |
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 overall structure is a bit hard to understand.
It's currently:
if (a && b) {
warning(c, ...)
return true
} else if (a) {
warning(c, ...)
// warnedProps[...] = true <-- missing for some reason
// return true <-- missing for some reason
}
Can you think of ways to simplify it?
For example:
if (a && !c) {
if (b) {
warning(false, ...)
} else {
warning(false, ...)
}
warnedProps[...] = true
return true
}
@@ -189,8 +200,6 @@ if (__DEV__) { | |||
name, | |||
getStackAddendum(), | |||
); | |||
warnedProperties[name] = true; | |||
return true; |
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.
Why was this removed?
if (typeof value === 'boolean') { | ||
if (value === true) { | ||
warning( | ||
DOMProperty.shouldAttributeAcceptBooleanValue(name), |
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 we make this warning(false
and move out this (negated) condition together with the top one?
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.
Yes :) Just so I have a correct understanding, the refactor will look like:
if (typeof value === 'boolean' && !false) {
if (value === true) {
warning(false,
...
);
} else {
warning(false,
...
);
}
...
}
Is this the branching logic you prefer? I do not understand the !false condition, so if I am incorrect please explain what should take it's place.
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 (typeof value === 'boolean' && !DOMProperty.shouldAttributeAcceptBooleanValue(name)) {
if (value === true) {
warning(false,
...
);
} else {
warning(false,
...
);
}
...
}
@@ -54,8 +54,8 @@ describe('ReactDOM unknown attribute', () => { | |||
expectDev( | |||
normalizeCodeLocInfo(console.error.calls.argsFor(0)[0]), | |||
).toMatch( | |||
'Warning: Received `true` for non-boolean attribute `unknown`. ' + | |||
'If this is expected, cast the value to a string.\n' + | |||
'Warning: Received `true` for non-boolean attribute `unknown`. If this is expected, cast ' + |
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 change is unnecessary, right? It just reformats the message in a slightly less readable way.
@@ -2042,7 +2042,8 @@ describe('ReactDOMComponent', () => { | |||
expect(el.hasAttribute('whatever')).toBe(false); | |||
|
|||
expectDev(console.error.calls.argsFor(0)[0]).toContain( | |||
'Warning: Received `true` for non-boolean attribute `whatever`', | |||
'Received `true` for non-boolean attribute `whatever`. If this is expected, cast ' + |
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.
Would look nicer if If this is expected...
started on the new line in test.
@@ -2055,7 +2056,8 @@ describe('ReactDOMComponent', () => { | |||
expect(el.hasAttribute('whatever')).toBe(false); | |||
|
|||
expectDev(console.error.calls.argsFor(0)[0]).toContain( | |||
'Warning: Received `true` for non-boolean attribute `whatever`', | |||
'Received `true` for non-boolean attribute `whatever`. If this is expected, cast ' + |
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.
Same
@@ -2230,7 +2232,8 @@ describe('ReactDOMComponent', () => { | |||
expect(el.hasAttribute('whatever')).toBe(false); | |||
|
|||
expectDev(console.error.calls.argsFor(0)[0]).toContain( | |||
'Warning: Received `true` for non-boolean attribute `whatever`.', | |||
'Received `true` for non-boolean attribute `whatever`. If this is expected, cast ' + |
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.
Same
'Warning: Received `false` for non-boolean attribute `whatever`.', | ||
'If you mean to conditionally pass an attribute, use a ternary ' + | ||
'expression: `false`={condition ? value : undefined} instead of ' + | ||
'{condition && value}.', |
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.
What does false={condition? value : undefined}
mean? Why does it says false
?
Please fix this to show the attribute name.
} else { | ||
warning( | ||
false, | ||
'If you mean to conditionally pass an attribute, use a ternary ' + |
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 this also start with Received ... for non-boolean attribute ...
? I want both messages to look similar, but with different second sentences.
@@ -2283,7 +2286,9 @@ describe('ReactDOMComponent', () => { | |||
|
|||
expectDev(console.error.calls.count()).toBe(1); | |||
expectDev(console.error.calls.argsFor(0)[0]).toContain( | |||
'Warning: Received `false` for non-boolean attribute `whatever`.', | |||
'Received `false` for non-boolean attribute `whatever`. If you mean to conditionally ' + | |||
'pass an attribute, use a ternary expression: {`whatever` ? value : undefined} ' + |
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.
Please make sure to read the messages you assert in tests. It looks like you were trying to adjust tests to match the wrong behavior in the code, rather than the other way around.
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.
Sorry, my understanding was that the value being passed was the condition. So value
should hold the attribute name instead of condition
correct? It didn't make sense that whatever
(holding the value false
) could be an attribute. Thank you for your patience and help through this issue.
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’m sorry that I snapped. I had a bad day and shouldn’t have spoken to you with this tone. I apologize. In this test, whatever
is the attribute name. Not the value. You can see if from the first sentence:
Received
false
for non-boolean attributewhatever
.
Therefore, a condition like {whatever ? value : undefined}
is not relevant to the user code. The user has something like whatever={condition && value}
with condition
being false
. We want to tell the user to turn it into something like whatever={condition ? value : undefined}
.
So I think ideally the messages would be:
- If you get
true
:
expectDev(console.error.calls.argsFor(0)[0]).toContain(
'Received `true` for non-boolean attribute `whatever`.\n\n' +
'If you want `true` to appear in the DOM, cast it to a string. ' +
'For example, you can pass `whatever="true"` or `whatever={String(value)}` instead.'
);
- If you get
false
:
expectDev(console.error.calls.argsFor(0)[0]).toContain(
'Received `false` for non-boolean attribute `whatever`.\n\n' +
'If you want `false` to appear in the DOM, cast it to a string. ' +
'For example, you can pass `whatever="false"` or `whatever={String(value)}`.\n\n' +
'If you want to omit `whatever` based on a condition, use a ternary expression. ' +
'For example, you can pass `whatever={condition ? value : undefined}` ' +
'instead of `whatever={condition && value}`.'
);
In these examples, whatever
should of course refer to the attribute name, not literally "whatever". I only used whatever
because that's what the test already uses.
I hope this is helpful and clarifies things!
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.
No need to apologize, your message did not come off badly. I only apologized for making you revise this pr so many times because I did not grasp the whole picture. Yes, this is extremely helpful. Thank you.
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.
Do you prefer:
`whatever={String(value)}`
or
`whatever={value.toString()}`
I feel the second option is a bit more verbose and easier to read.
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 thought about the second one but it will throw for null/undefined. Might be unexpected. First one seems safer. Although on the other hand you probably don't want "undefined"
or "null"
as strings either.
I wish there was some concise way to express "stringify unless it's null/undefined".
I guess I like that value.toString()
throws it it doesn't exist. If you mess it up you'll immediately realize it by the crash, instead of quietly using the wrong value. So let's suggest that.
typeof value === 'boolean' && | ||
!DOMProperty.shouldAttributeAcceptBooleanValue(name) | ||
) { | ||
if (value === true) { |
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.
Nit: we already know it's a boolean so can just write if (value)
Thanks for the fixes! I looked at it in practice, and I think it's a bit too noisy with backticks everywhere. Let's keep them around singular words (e.g. around attribute name), but remove them around fragments of code. I also found the current messages too noisy and potentially misleading after all. Sorry about this—it takes a few iterations to get this right. Here's my new proposal. For expectDev(console.error.calls.argsFor(0)[0]).toContain(
'Received `true` for non-boolean attribute `whatever`.\n\n' +
'If you want to write it to the DOM, pass a string instead: ' +
'whatever="true" or whatever={value.toString()}.'
); For expectDev(console.error.calls.argsFor(0)[0]).toContain(
'Received `false` for non-boolean attribute `whatever`.\n\n' +
'If you want to write it to the DOM, pass a string instead: ' +
'whatever="false" or whatever={value.toString()}.\n\n' +
'If you conditionally omit it with whatever={condition && value}, ' +
'pass whatever={condition ? value : undefined} instead.'
); I hope it’s not too much of a burden! I think these are clearer. |
I think this is good to go. Did a few minor changes. Thanks so much! |
…11308) * Modified tests and corrected error message. facebook#3 * Fixed syntax issues. facebook#3 * Modified test. facebook#3 * Prettified. facebook#3 * Changed warning message to handle true and false boolean values. facebook#3 * Changed test to contain undefined instead of value. facebook#3 * Simplified branch structure. facebook#3 * Refactored branching logic. facebook#3 * Refactored falsy warning message and tests. facebook#3 * Changed condition to attribute name. facebook#3 * Refactored falsy and truthy warning messages with tests updated. facebook#3 * Added missing character. facebook#3 * Fixed warning message. facebook#3 * Cleared extra whitespace. facebook#3 * Refactored warning messages to be clear. facebook#3 * Prettified. facebook#3 * Grammar fix * Tweak unrelated warning The message didn't make sense because it appears for *any* attributes, not just numeric ones. * Tweak the message for more clarity * Add a special message for false event handlers * Add missing whitespace * Revert size changes
Before submitting a pull request, please make sure the following is done:
master
.yarn
in the repository root.yarn test
). Tip:yarn test --watch TestName
is helpful in development.yarn prettier
).yarn lint
). Tip:yarn linc
to only check changed files.yarn flow
).Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html