-
Notifications
You must be signed in to change notification settings - Fork 46.9k
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
Fix passing symbols and functions to textarea #13362
Fix passing symbols and functions to textarea #13362
Conversation
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.
Awesome! Thank you for the PR. I think @nhunzaker should also take a look because it's similar to his work in #11741. And maybe we want this for select as well?
We probably also need to check if we have to update the attribute snapshots: https://github.com/facebook/react/tree/master/fixtures/attribute-behavior
@@ -20,6 +20,8 @@ describe('ReactDOMTextarea', () => { | |||
let renderTextarea; | |||
|
|||
beforeEach(() => { | |||
jest.resetModules(); |
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.
Is there any particular reason for this addition? I think the only reason we do that is when we have tests that require multiple separate versions of React at the same time.
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.
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.
Ah, right, makes sense. Otherwise the warning will only pop up once 👍
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.
Btw, thanks for the feedback. I really appreciate it 🙂
@@ -116,7 +117,9 @@ export function initWrapperState(element: Element, props: Object) { | |||
} | |||
|
|||
node._wrapperState = { | |||
initialValue: '' + initialValue, | |||
initialValue: getSafeValue( | |||
props.value != null ? props.value : initialValue, |
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 seems to be different than the previous implementation. Do you mind explaining why? 🙂
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.
Ah, you're right. This has to be a mistake. I took influence from ReactDOMFiberInput
and the files got oddly similar under cognitive load.
Instead, it can simply be getSafeValue(initialValue)
, I think :-)
Thanks!
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.
initialValue
already references props.value
on L:83 so this logic is redundant.
ReactDOM.render(<textarea defaultValue={Symbol('foobar')} />, container); | ||
const node = container.firstChild; | ||
|
||
// TODO: add warnings once defaultValue is compatible for them |
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 quite understand this comment. @nhunzaker I think this is copied over from #11741 - Can you point me to something so I can read up on that?
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, should've been more thorough. I've added this comment as a reminder for future references once this PR is good to go. Currently there are no warnings for reserved props in React and hence the TODOs in @nhunzaker's PR and this 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.
I know I'm the first offender here (what is // TODO: we should warn here.
even? 😿 ), but could you change this comment to something like:
// TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
@@ -126,7 +127,7 @@ export function updateWrapper(element: Element, props: Object) { | |||
if (value != null) { | |||
// Cast `value` to a string to ensure the value is set correctly. While | |||
// browsers typically do this as necessary, jsdom doesn't. | |||
const newValue = '' + value; | |||
const newValue = getSafeValue(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.
I have a question about this change - could someone please be more specific about which cases require the casting done previously ('' + value
) and what exactly breaks by removing it from here and the previous lines?
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.
My first recollection is that we don't need to convert these to string values, but we didn't update all of the locations because it wasn't specific to the PR.
Inputs went through a lot of fixes as we ironed out all of the edge cases for special input types on Chrome/Safari, so to some degree, touching more than we needed to felt scary 🙈.
I think it would be good to take a second look at this.
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.
Looks good at first pass. I'm doing some travel today, but I'll be able to give a more thorough review once I'm settled.
Thanks for sending this out!
@@ -126,7 +127,7 @@ export function updateWrapper(element: Element, props: Object) { | |||
if (value != null) { | |||
// Cast `value` to a string to ensure the value is set correctly. While | |||
// browsers typically do this as necessary, jsdom doesn't. | |||
const newValue = '' + value; | |||
const newValue = getSafeValue(value); | |||
|
|||
// To avoid side effects (such as losing text selection), only set value if changed | |||
if (newValue !== node.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.
Ah right. We need the new value to be a string for comparison (edit) because the value always reports as a string.
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.
That does make sense to me. Is there a particular reason though why the casting is done by concatenation instead of e.g. String(value)
? Because with Symbols it produces a TypeError when trying to concat a Symbol and a string.
Moreover, if a textarea
receives for i.e a function as its value
and we concat it into a string, then getSafeValue
would fail its check and append it as a valid 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.
Maybe we could add a similar check like in ReactDOMFiberInput?
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.
Is there a particular reason though why the casting is done by concatenation instead of e.g.
String(value)
?
There were some performance concerns about String()
: #11741 (comment)
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.
because the value always reports as a string
I was wondering why Flow did not catch that and found the issue in the getSafeValue()
function. Not sure if we want the fix since it adds some complexity but it's nice to know that all code paths indeed work. Check out: #13367
This is an attempt in improving the soundness for the safe value cast that was added in facebook#11741. We want this to avoid situations like [this one](facebook#13362 (comment)) where we need to remember why we have certain type casts. Additionally we can be sure that we only cast safe values to string. The problem was `getSafeValue()`. It used the (deprecated) `*` type to infer the type which resulted in a passing-through of the implicit `any` of the props `Object`. So `getSafeValue()` was effectively returning `any`. Once I fixed this, I found out that Flow does not allow concatenating all possible types to a string (e.g `"" + false` fails in Flow). To fix this as well, I've opted into making the SafeValue type opaque and added a function that can be used to get the string value. This is sound because we know that SafeValue is already checked. I've verified that the interim function is inlined by the compiler and also looked at a diff of the compiled react-dom bundles to see if I've regressed anything. Seems like we're good.
* Improve soundness of ReactDOMFiberInput typings This is an attempt in improving the soundness for the safe value cast that was added in #11741. We want this to avoid situations like [this one](#13362 (comment)) where we need to remember why we have certain type casts. Additionally we can be sure that we only cast safe values to string. The problem was `getSafeValue()`. It used the (deprecated) `*` type to infer the type which resulted in a passing-through of the implicit `any` of the props `Object`. So `getSafeValue()` was effectively returning `any`. Once I fixed this, I found out that Flow does not allow concatenating all possible types to a string (e.g `"" + false` fails in Flow). To fix this as well, I've opted into making the SafeValue type opaque and added a function that can be used to get the string value. This is sound because we know that SafeValue is already checked. I've verified that the interim function is inlined by the compiler and also looked at a diff of the compiled react-dom bundles to see if I've regressed anything. Seems like we're good. * Fix typo
@@ -11,6 +11,7 @@ import invariant from 'shared/invariant'; | |||
import warning from 'shared/warning'; | |||
|
|||
import ReactControlledValuePropTypes from '../shared/ReactControlledValuePropTypes'; | |||
import getSafeValue from './getSafeValue'; | |||
|
|||
let didWarnValDefaultVal = false; | |||
|
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 type of initialValue in the _wrapperState below should probably be SafeValue
now, just like we did for input elements.
Edit: Nevermind you have to push your changes first 🙈
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.
Just pushed my changes - I hope they make sense and I took the right approach :-)
@@ -54,7 +55,7 @@ export function getHostProps(element: Element, props: Object) { | |||
...props, | |||
value: undefined, | |||
defaultValue: undefined, | |||
children: '' + node._wrapperState.initialValue, | |||
children: safeValueToString(getSafeValue(node._wrapperState.initialValue)), |
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 just do safeValueToString(node._wrapperState.initialValue)
instead here?
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, this seems correct 👍 . _wrapperState
is only set once and that is already a SafeValue
.
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.
Thanks for updating the PR against my changes so quickly 🙂 If you want, you can take a look at as well.
@@ -54,7 +55,7 @@ export function getHostProps(element: Element, props: Object) { | |||
...props, | |||
value: undefined, | |||
defaultValue: undefined, | |||
children: '' + node._wrapperState.initialValue, | |||
children: safeValueToString(getSafeValue(node._wrapperState.initialValue)), |
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, this seems correct 👍 . _wrapperState
is only set once and that is already a SafeValue
.
@@ -107,7 +109,7 @@ export function initWrapperState(element: Element, props: Object) { | |||
children = children[0]; | |||
} | |||
|
|||
defaultValue = '' + children; | |||
defaultValue = getSafeValue(children); |
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.
defaultValue
was a string before and is a SafeValue now. I'm not sure if this causes issues with the equality test below.
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.
Should we stringify here as well, just to be safe?
// To avoid side effects (such as losing text selection), only set value if changed | ||
if (newValue !== node.value) { | ||
node.value = newValue; | ||
if ((value: any) !== node.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.
We compared this against a string
before and now it's a SafeValue
. Is this intended? I think this means when value is a non-string (i.e a number) it will always update node.value
since the strict equality will always return false.
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.
Hm, right. Would it make more sense if the comparison was props.value !== node.value
and use getSafeValue
for const newValue
?
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.
That would still false if props.value = 1
and node.value = '1'
. I think bringing back const newValue
as safeValueToString(value)
is more correct.
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.
Gotcha. Pushed a change with regards to this.
Following up on the changes I made in facebook#13367, @gaearon suggest that "safe" could be read as necessary for security. To avoid misleading a reader, I'm changing the name. A few names where discussed in the previous PR. I think `ToStringValue` makes sense since the value itself is not a string yet but an opaque type that can be cast to a string. For the actual string concatenation, I've used `toString` now to avoid confusion: `toStringValueToString` is super weird and it's namespaced anyhow. Definitely open for suggestions here. :) I'll wait until we wrap up facebook#13362 and take care of rebase afterwards.
Following up on the changes I made in #13367, @gaearon suggest that "safe" could be read as necessary for security. To avoid misleading a reader, I'm changing the name. A few names where discussed in the previous PR. I think `ToStringValue` makes sense since the value itself is not a string yet but an opaque type that can be cast to a string. For the actual string concatenation, I've used `toString` now to avoid confusion: `toStringValueToString` is super weird and it's namespaced anyhow. Definitely open for suggestions here. :) I'll wait until we wrap up #13362 and take care of rebase afterwards.
Synced up with the changes from #13376. |
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.
Changes look good from my end.
Rebased and fixed conflict derived from #13361. |
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.
Looks great already! Just one comment.
@@ -110,7 +112,7 @@ export function initWrapperState(element: Element, props: Object) { | |||
children = children[0]; | |||
} | |||
|
|||
defaultValue = '' + children; | |||
defaultValue = getToStringValue(children); |
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 find out if we really need to convert to ToStringValue here?
I think we should be able to just pass children
directly as the defaultValue
since we convert to the ToStringValue
down below 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.
You're right, this does seem redundant. Removed it, thanks!
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 looks perfect now. Thank you very much 🙂 If you want a follow-up task, you could work on the same fix for elements.
Taking a quick pass to verify this with the fixtures, but this looks good. |
Thanks! |
My pleasure 🙂 |
Somewhat related to #11734. Builds on top of @nhunzaker's work on
ReactDOMFiberInput
Reproduced cases that work incorrectly (?) as of today (https://codesandbox.io/s/pj40nrp5px):