-
Notifications
You must be signed in to change notification settings - Fork 47.3k
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 uncontrolled radios #10156
Fix uncontrolled radios #10156
Changes from all commits
ce70f13
25cff31
84bda97
48fc77f
15228e8
dede4b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import React from 'react'; | ||
|
||
import Fixture from '../../Fixture'; | ||
|
||
class RadioGroupFixture extends React.Component { | ||
constructor(props, context) { | ||
super(props, context); | ||
|
||
this.state = { | ||
changeCount: 0, | ||
}; | ||
} | ||
|
||
handleChange = () => { | ||
this.setState(({changeCount}) => { | ||
return { | ||
changeCount: changeCount + 1, | ||
}; | ||
}); | ||
}; | ||
|
||
handleReset = () => { | ||
this.setState({ | ||
changeCount: 0, | ||
}); | ||
}; | ||
|
||
render() { | ||
const {changeCount} = this.state; | ||
const color = changeCount === 2 ? 'green' : 'red'; | ||
|
||
return ( | ||
<Fixture> | ||
<label> | ||
<input | ||
defaultChecked | ||
name="foo" | ||
type="radio" | ||
onChange={this.handleChange} | ||
/> | ||
Radio 1 | ||
</label> | ||
<label> | ||
<input name="foo" type="radio" onChange={this.handleChange} /> | ||
Radio 2 | ||
</label> | ||
|
||
{' '} | ||
<p style={{color}}> | ||
<code>onChange</code>{' calls: '}<strong>{changeCount}</strong> | ||
</p> | ||
<button onClick={this.handleReset}>Reset count</button> | ||
</Fixture> | ||
); | ||
} | ||
} | ||
|
||
export default RadioGroupFixture; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
|
||
'use strict'; | ||
|
||
var {ELEMENT_NODE} = require('HTMLNodeType'); | ||
import type {Fiber} from 'ReactFiber'; | ||
import type {ReactInstance} from 'ReactInstanceType'; | ||
|
||
|
@@ -23,6 +24,9 @@ type ValueTracker = { | |
type WrapperState = {_wrapperState: {valueTracker: ?ValueTracker}}; | ||
type ElementWithWrapperState = Element & WrapperState; | ||
type InstanceWithWrapperState = ReactInstance & WrapperState; | ||
type SubjectWithWrapperState = | ||
| InstanceWithWrapperState | ||
| ElementWithWrapperState; | ||
|
||
var ReactDOMComponentTree = require('ReactDOMComponentTree'); | ||
|
||
|
@@ -43,15 +47,11 @@ function getTracker(inst: any) { | |
return inst._wrapperState.valueTracker; | ||
} | ||
|
||
function attachTracker(inst: InstanceWithWrapperState, tracker: ?ValueTracker) { | ||
inst._wrapperState.valueTracker = tracker; | ||
function detachTracker(subject: SubjectWithWrapperState) { | ||
subject._wrapperState.valueTracker = null; | ||
} | ||
|
||
function detachTracker(inst: InstanceWithWrapperState) { | ||
delete inst._wrapperState.valueTracker; | ||
} | ||
|
||
function getValueFromNode(node) { | ||
function getValueFromNode(node: any) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general we should avoid adding (I don’t mean this particular case is problematic, but this is something to always keep in mind.) |
||
var value; | ||
if (node) { | ||
value = isCheckable(node) ? '' + node.checked : node.value; | ||
|
@@ -113,40 +113,46 @@ var inputValueTracking = { | |
return getTracker(ReactDOMComponentTree.getInstanceFromNode(node)); | ||
}, | ||
|
||
trackNode: function(node: ElementWithWrapperState) { | ||
if (node._wrapperState.valueTracker) { | ||
trackNode(node: ElementWithWrapperState) { | ||
if (getTracker(node)) { | ||
return; | ||
} | ||
node._wrapperState.valueTracker = trackValueOnNode(node, node); | ||
}, | ||
|
||
track: function(inst: InstanceWithWrapperState) { | ||
track(inst: InstanceWithWrapperState) { | ||
if (getTracker(inst)) { | ||
return; | ||
} | ||
var node = ReactDOMComponentTree.getNodeFromInstance(inst); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked at the stacktrace and it's crashing here (rather than in the other place I expected). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ooo I think I understand where the bug is |
||
attachTracker(inst, trackValueOnNode(node, inst)); | ||
inst._wrapperState.valueTracker = trackValueOnNode(node, inst); | ||
}, | ||
|
||
updateValueIfChanged(inst: InstanceWithWrapperState | Fiber) { | ||
if (!inst) { | ||
updateValueIfChanged(subject: SubjectWithWrapperState | Fiber) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was there any specific reason for changing terminology here? Did inputs change? Or was existing terminology inconsistent? In Stack, we used There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The inputs changed as far as I can know. The only place this was called otherwise was in the ChangeEventPlugin where it's just called an "instance" It may very well be a DOM node there in Fiber (now that I think of it). The need for the logic branch here is that we need the actual DOM node, and to handle both renderers that means calling As for the terminology it was more to save line space instead of writing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do I understand correctly that now the code supports passing three different types? Stack instance, Fiber, and a DOM node. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TBH I don't know what the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. I was under impression it was always passed a Fiber or a Stack instance before this change. Since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That may be! I still unfamiliar with the Fiber data types but the only place this is called is is ChangeEventPlugin with the Is the Fiber situation that sometimes it may be an "instance" (DOM Node) and sometimes it may be a "Fiber". In the Stack case it's always an internal Instance There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll check that, thanks. |
||
if (!subject) { | ||
return false; | ||
} | ||
var tracker = getTracker(inst); | ||
var tracker = getTracker(subject); | ||
|
||
if (!tracker) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm having trouble understanding how to hit this code path. In which case do we not have a tracker already? I assumed we always have it since we create it during mount. |
||
if (typeof (inst: any).tag === 'number') { | ||
inputValueTracking.trackNode((inst: any).stateNode); | ||
if (typeof (subject: any).tag === 'number') { | ||
inputValueTracking.trackNode((subject: any).stateNode); | ||
} else { | ||
inputValueTracking.track((inst: any)); | ||
inputValueTracking.track((subject: any)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This branching looks odd to me. It seems like it tries to branch on Fiber and Stack code. However now I think this shows why it might be better to reduce the polymorphism here and maybe change this to only accept DOM nodes. But I'd also like to understand why tests (including fixtures) didn't catch this. It is not quite obvious to me. |
||
} | ||
return true; | ||
} | ||
|
||
var lastValue = tracker.getValue(); | ||
var nextValue = getValueFromNode( | ||
ReactDOMComponentTree.getNodeFromInstance(inst), | ||
); | ||
|
||
var node = subject; | ||
|
||
// TODO: remove check when the Stack renderer is retired | ||
if ((subject: any).nodeType !== ELEMENT_NODE) { | ||
node = ReactDOMComponentTree.getNodeFromInstance(subject); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain more about why this change is necessary? I still don’t quite get why it was called unconditionally, but now is called conditionally, even though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, I misread. I now see that previous code also had (a different) check. I wonder if changing that check is what caused the issue. I'm still not sure why though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm no I didn’t misread 😛 This does look like a new check. So my previous comment still stands. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. more in the comment above, but the check is purely to avoid calling |
||
} | ||
|
||
var nextValue = getValueFromNode(node); | ||
|
||
if (nextValue !== lastValue) { | ||
tracker.setValue(nextValue); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -860,6 +860,9 @@ ReactDOMComponent.Mixin = { | |
// happen after `_updateDOMProperties`. Otherwise HTML5 input validations | ||
// raise warnings and prevent the new value from being assigned. | ||
ReactDOMInput.updateWrapper(this); | ||
// We also check that we haven't missed a value update, such as a | ||
// Radio group shifting the checked value to another named radio input. | ||
inputValueTracking.updateValueIfChanged(this); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the actual fix |
||
break; | ||
case 'textarea': | ||
ReactDOMTextarea.updateWrapper(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.
Doing some local testing, looks like this causes a failure on line 103:
https://github.com/jquense/react/blob/15228e8b5f9d056d475955a54723cd906ffa5619/fixtures/dom/src/components/TestCase.js#L103
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.
Last time I do merge conflict resolve on github :P man.