-
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
🔥 Stop syncing the value attribute on inputs (behind a feature flag) #13526
🔥 Stop syncing the value attribute on inputs (behind a feature flag) #13526
Conversation
@@ -1605,15 +1605,15 @@ describe('ReactDOMInput', () => { | |||
}; | |||
} | |||
|
|||
it('always sets the attribute when values change on text inputs', function() { | |||
it('retains the initial value attribute when values change on text inputs', function() { |
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's always been this way, but I don't like how React stores an initial value:
https://github.com/facebook/react/blob/master/packages/react-dom/src/client/ReactDOMFiberInput.js#L25
I think the value should just be blank when it becomes null, or revert to the defaultValue prop (which could be null).
ReactDOM: size: -0.2%, gzip: -0.5% Details of bundled changes.Comparing: 2d5c590...5bb2e39 react-dom
schedule
Generated by 🚫 dangerJS |
10473d0
to
ffb38e6
Compare
Is there any other logic we can remove or simplify now? I was hoping some of the things we added over time might be unnecessary now. Although I'm aware some of your fixes were for issues that predated #6406. |
I talked to @sebmarkbage, and the guiding principles we came up with are:
Our goal is to simplify the system and reduce the edge cases. So if we only set it out of courtesy for e.g. CSS selectors, I think we should stop doing that, and keep it minimal. Just let Regarding We should not support |
I was too. I'll take a second pass through. For everything else, I've started a checklist in the description of things I need to do. |
You might also want to start by adding a feature flag for this. Because we'll want to keep both versions around for a while. It might be reasonable to fork some files like |
ffb38e6
to
0715051
Compare
I did some more work today that I believe gets us most of the way there. I shouldn't continue to refine this anymore, next I need to:
I'm really curious about the final item. How should SSR work when I'm also starting to think that the rules are simple enough now that we can start to think about this more as a spec. I think I'd like to write up some rules, even if it's just for myself. |
One idea for SSR is to very strictly adhere to "value sets the property, defaultValue sets the attribute". For this to work, we'd need to allow a user to set both properties at once without warning. I don't necessarily sponsor this idea 😸. This would be a breaking change for just about every server-side app, but it would keep things consistent in both environments. It also requires a user to be intentional about setting the value attribute for sensitive information, like passwords, which could be a desired side-effect for some users. |
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 introduce a new feature flag for this. Let's call it something like disableInputAttributeSyncing
. We need to be granular about changes because we might want to try them separately at FB, so I don't want to have just one feature flag.
We want to run relevant tests in both modes. But I think we don't want to run the whole bundle in two modes just yet.
So I propose that for now you can create ReactFire-test.internal.js
and copy paste any tests where behavior differs into it, and adjust the expectations. And set the feature flag at the top of this file.
Then we can get it in and start testing at FB. It doesn't have to be 100% complete for this — since it won't be enabled by default. For example SSR stuff can wait.
5bb2e39
to
f8c710c
Compare
// manual testing in the fixtures shows that the active element | ||
// is no longer the input, however blur() + a blur event seem to | ||
// be the only way to remove focus in JSDOM | ||
node.blur() |
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.
@gaearon this was the false positive case mentioned earlier. JSDOM doesn't update the active element on blur, only after blur() + a blur event.
I have also walked through the number fixtures to verify that things continue to work as expected manually.
@@ -57,13 +58,14 @@ function isControlled(props) { | |||
|
|||
export function getHostProps(element: Element, props: Object) { | |||
const node = ((element: any): InputWithWrapperState); | |||
const checked = props.checked; | |||
const checked = | |||
props.checked != null ? props.checked : node._wrapperState.initialChecked; |
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 doing props.checked
access twice now. Let's avoid changes like 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.
Removed in 14dab53
const currentValue = node.value; | ||
const value = disableInputAttributeSyncing | ||
? getToStringValue(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.
Seems like previous we didn't have to call this earlier than next condition. What changed? Let's avoid adding more eager computation.
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.
getToStringValue
is called when assigning initialValue
in the wrapper state:
react/packages/react-dom/src/client/ReactDOMInput.js
Lines 120 to 122 in 7204b63
initialValue: getToStringValue( | |
props.value != null ? props.value : defaultValue, | |
), |
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.
Something can be done here. Working on it.
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 was! I did this in addffb8
@@ -63,7 +64,7 @@ export function getHostProps(element: Element, props: Object) { | |||
defaultChecked: undefined, | |||
defaultValue: undefined, | |||
value: undefined, | |||
checked: checked != null ? checked : node._wrapperState.initialChecked, | |||
checked: props.checked != null ? props.checked : node._wrapperState.initialChecked |
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, I'm being silly. I'll revert this too
return; | ||
} | ||
|
||
const initialValue = toString(node._wrapperState.initialValue); | ||
const currentValue = 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.
I inlined this value because it is only needed in one place in both feature flag branches.
I believe all comments have been addressed and, reading through the code, I feel good about this change. |
520c127
to
fa10f5f
Compare
// 1. The value React property when present | ||
// 2. The defaultValue React property when present | ||
// 3. An empty string | ||
if (initialValue !== 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.
This used to compare toString(node._wrapperState.initialValue) !== node.value
.
But now it compares node._wrapperState.initialValue !== node.value
.
Is that change safe?
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 are correct. This should call toString()
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.
Needs a regression test?
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.
Hard. We only need to call toString for value comparison so that we do not reassign the existing default value and trigger validation. null
and undefined
are already skipped. This also happens on the post mount step, and not during hydration. I'm not sure how to write a test for it.
We have a manual test fixture for this reason (sorry, I know the formatting isn't ideal):
react/fixtures/dom/src/components/fixtures/text-inputs/index.js
Lines 65 to 99 in a7bd7c3
<Fixture> | |
<form className="control-box"> | |
<fieldset> | |
<legend>Empty value prop string</legend> | |
<input value="" required={true} /> | |
</fieldset> | |
<fieldset> | |
<legend>No value prop</legend> | |
<input required={true} /> | |
</fieldset> | |
<fieldset> | |
<legend>Empty defaultValue prop string</legend> | |
<input required={true} defaultValue="" /> | |
</fieldset> | |
<fieldset> | |
<legend>No value prop date input</legend> | |
<input type="date" required={true} /> | |
</fieldset> | |
<fieldset> | |
<legend>Empty value prop date input</legend> | |
<input type="date" value="" required={true} /> | |
</fieldset> | |
</form> | |
</Fixture> | |
<p className="footnote"> | |
Checking the date type is also important because of a prior fix for | |
iOS Safari that involved assigning over value/defaultValue | |
properties of the input to prevent a display bug. This also triggers | |
input validation. | |
</p> | |
<p className="footnote"> | |
The date inputs should be blank in iOS. This is not a bug. | |
</p> | |
</TestCase> |
export function toString(value: ToStringValue): string { | ||
return '' + (value: any); | ||
export function toString(value: ?ToStringValue): string { | ||
return value == null ? '' : '' + (value: any); |
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 this change? Did we check every toString
callsite to ensure it doesn't break 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.
There were two places in postMount where it could stringify null or undefined. Otherwise this can stay the same. Updating now.
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 priority here is that the old code path stays as similar as it can be. So that we can merge this without worrying.
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.
Totally. I've done this in e4f68fe
|
||
if (ReactFeatureFlags.disableInputAttributeSyncing) { | ||
// TODO: figure out why. This might be a bug. | ||
expect(called).toBe(1); |
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's possible that this happens because value
isn't present, so no operation occurs on value
, so value tracking is never triggered. A solution could be to set the value from defaultValue or manually call tracking.
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.
Old code paths look legit.
I'll need to take a closer look at new code paths later but we can get this in and iterate. Thank you!
…acebook#13526) * 🔥 Stop syncing the value attribute on inputs * Eliminate some additional checks * Remove initialValue and initialWrapper from wrapperState flow type * Update tests with new sync logic, reduce some operations * Update tests, add some caveats for SSR mismatches * Revert newline change * Remove unused type * Call toString to safely type string values * Add disableInputAttributeSyncing feature flag Reverts tests to original state, adds attribute sync feature flag, then moves all affected tests to ReactFire-test.js. * Revert position of types in toStringValues * Invert flag on number input blur * Add clarification why double blur is necessary * Update ReactFire number cases to be more explicite about blur * Move comments to reduce diff size * Add comments to clarify behavior in each branch * There is no need to assign a different checked behavior in Fire * Use checked reference * Format * Avoid precomputing stringable values * Revert getToStringValue comment * Revert placement of undefined in getToStringValue * Do not eagerly stringify value * Unify Fire test cases with normal ones * Revert toString change. Only assign unsynced values when not nully
…acebook#13526) * 🔥 Stop syncing the value attribute on inputs * Eliminate some additional checks * Remove initialValue and initialWrapper from wrapperState flow type * Update tests with new sync logic, reduce some operations * Update tests, add some caveats for SSR mismatches * Revert newline change * Remove unused type * Call toString to safely type string values * Add disableInputAttributeSyncing feature flag Reverts tests to original state, adds attribute sync feature flag, then moves all affected tests to ReactFire-test.js. * Revert position of types in toStringValues * Invert flag on number input blur * Add clarification why double blur is necessary * Update ReactFire number cases to be more explicite about blur * Move comments to reduce diff size * Add comments to clarify behavior in each branch * There is no need to assign a different checked behavior in Fire * Use checked reference * Format * Avoid precomputing stringable values * Revert getToStringValue comment * Revert placement of undefined in getToStringValue * Do not eagerly stringify value * Unify Fire test cases with normal ones * Revert toString change. Only assign unsynced values when not nully
Is there a way to toggle this feature flag without recompiling react? |
Is there a reason this hasn't been enabled by default? It seems to be a cause for the bug #17609. |
No particular reason other than it's a breaking change so we can't enable it until 17. |
This PR revives #10150, my original PR to remove value attribute syncing.
TODOs:
This fixture build demonstrates the behavior:
http://react-dom-no-sync-value.surge.sh/
See #11896 and #13525
🔥 🔥 🔥