Skip to content
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

Merged
merged 6 commits into from Jul 13, 2017
Merged

Conversation

jquense
Copy link
Contributor

@jquense jquense commented Jul 12, 2017

Fixes #9988, this is against master but we should cherry-pick the change back to 15.6 for a patch release.

I added a fixture instead of a test because, writing a unit test that reproduced the issue was confusingly hard actually.

Most of the extra bits here are to handle both fiber and stack, the content of the PR is really the one line in DOMComponent.

REACT_PATH = 'https://unpkg.com/react@' + version + '/dist/react.min.js';
DOM_PATH = 'https://unpkg.com/react-dom@' + version + '/dist/react-dom.min.js';
REACT_PATH = 'https://unpkg.com/react@' + version + '/dist/react.js';
DOM_PATH = 'https://unpkg.com/react-dom@' + version + '/dist/react-dom.js';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched to the unminified versions intentionally so was easier to step through stuff in devtools

@@ -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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual fix

@aweary aweary mentioned this pull request Jul 12, 2017
12 tasks
function attachTracker(inst: InstanceWithWrapperState, tracker: ?ValueTracker) {
inst._wrapperState.valueTracker = tracker;
function detachTracker(subject: SubjectWithWrapperState) {
delete subject._wrapperState.valueTracker;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use = null instead? delete causes deopts in my experience.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so. I think originally I was being overly cautious about expected properties sticking around.

);

var node = inst;
if (!(inst: any).nodeName) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you comment on this part? What are you trying to detect?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah its not great, I'm trying to detect if inst is a component instance, like it is in stack, or a DOM node as in fiber. This is the most minimal DRY change I could think of. Alternatively we could split this method into two, but its a bunch of duplication.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use same check as in here and leave a TODO to remove it when Stack is removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect 👍

@gaearon
Copy link
Collaborator

gaearon commented Jul 12, 2017

Looks like Stack tests are failing

 FAIL  src/renderers/dom/shared/__tests__/inputValueTracking-test.js
  ● inputValueTracking › should stop tracking

    expect(received).toBe(expected)
    
    Expected value to be (using ===):
      false
    Received:
      true

Also prettier is unhappy.

@jquense
Copy link
Contributor Author

jquense commented Jul 12, 2017

not sure what the deal with prettier is, i ran it locally. lemme mess around with it. Do I have to do something special to run tests against the stack renderer?

@jquense
Copy link
Contributor Author

jquense commented Jul 12, 2017

o nvm on the failing test, that's my bad. sorry

@nhunzaker
Copy link
Contributor

@jquense This looks great from my end. I'd be happy to take on some manual browser testing. What should I test for?

Copy link
Contributor

@nhunzaker nhunzaker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside from the reference error (covered by lint), this checks out in:

  • IE11
  • IE15
  • Chrome 59
  • Safari 10
  • Firefox 54

!version || !resolvedIn || semver.gte(version, resolvedIn);

complete = !isTestRelevant || complete;
complete = !isTestFixed || complete;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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.

@nhunzaker
Copy link
Contributor

Here's a test build with the lint fix:
http://react-fix-uncontrolled-radios.surge.sh/

@jquense
Copy link
Contributor Author

jquense commented Jul 13, 2017

I don't think this change could change behavior for controlled inputs but technically it's doing slightly more work on every input update. I did run the various fixtures for input changes and didn't see anything but just noting it for posterity.

@nhunzaker nhunzaker merged commit 999df3e into facebook:master Jul 13, 2017
@nhunzaker
Copy link
Contributor

Checks out for me. @jquense Could you handle bringing this over to 15.x?

@gaearon
Copy link
Collaborator

gaearon commented Jul 13, 2017

AFAIK @flarnie's not working on 15.6.x right now. Do you want to handle this release? Would be your first release :-)

@nhunzaker
Copy link
Contributor

Well if you put it that way.

Sure

@gaearon
Copy link
Collaborator

gaearon commented Jul 14, 2017

It's yours! Look at what we did for previous releases: merge things to a branch freshly based of 15-stable, let me know when it's ready and we can get it out.

}

function getValueFromNode(node) {
function getValueFromNode(node: any) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general we should avoid adding any wherever possible. It very often leads to bugs. I know we used it sometimes, but I just want to point out for future reviews that it should be used only as last measure.

(I don’t mean this particular case is problematic, but this is something to always keep in mind.)

},

updateValueIfChanged(inst: InstanceWithWrapperState | Fiber) {
if (!inst) {
updateValueIfChanged(subject: SubjectWithWrapperState | Fiber) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 instance for internal instance, but in Fiber we use it for abstract renderer-specific instance (such as DOM node in case of ReactDOM). So both seemed fitting to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 getNodeFromInstance which seems to be fairly unforgiving of you passing in a node already, which I think is somerhow what's happening

As for the terminology it was more to save line space instead of writing InstanceWithWrapperState | ElementWithWrapperState in a few places.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

@jquense jquense Jul 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I don't know what the Fiber type is/does here. I didn't add the types originally and AFAICT its never passed a Fiber, only an instance or Dom node

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 .tag check is for detecting Fibers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 targetInst parameter and DOMComponent. I'm unsure what the case is in the changeEventPlugin.

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

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check that, thanks.


// TODO: remove check when the Stack renderer is retired
if ((subject: any).nodeType !== ELEMENT_NODE) {
node = ReactDOMComponentTree.getNodeFromInstance(subject);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 subject (aka inst) type has not changed (or has it?)

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 getNodeFromInstance when inst is already a Node, as in the Fiber case. The only reason it was added was to handle both renderers, You can see in backport PR that this file isn't even touched (i'm guessing that one probably doesn't suffer this bug btw)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Regression: onChange doesn't fire with defaultChecked and radio inputs
4 participants