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

Use the virtual target for change events to avoid restoring controlled state on the real target #10444

Merged
merged 5 commits into from
Aug 12, 2017

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Aug 11, 2017

Fixes #10420

The problem is that we try to restore the controlled value after the oninput event. So we've restored it to its previous value by the time we fire onChange. So the controlling component doesn't have a chance to update it.

This ensures that we "restore controlled state" on the wrapper instead of the real target - which is a noop.

@@ -24,8 +48,14 @@ class SelectFixture extends React.Component {
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="gree">Green</option>
Copy link
Contributor

Choose a reason for hiding this comment

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

🙈. I wonder who did this.

@sebmarkbage
Copy link
Collaborator Author

I think this could be related to: 0b220d0

@jquense Do you have an idea why that would be? Input events no longer fire onChange on <select />?

@sebmarkbage sebmarkbage changed the title Repro Case For Nested Controlled Select Fire onChange Event Even For onInput Events Aug 11, 2017
@sebmarkbage
Copy link
Collaborator Author

sebmarkbage commented Aug 11, 2017

@jquense This fix seems to fix it but I'm not sure if that breaks something else. 2348742

The fixture test seems to work, but the fixture selector itself seems broken. :) So it does break something.

@sebmarkbage sebmarkbage requested a review from jquense August 11, 2017 23:33
@jquense
Copy link
Contributor

jquense commented Aug 11, 2017

arg this is was beyond weird. yeah I originally had #10238 do this but ran it the issue you are seeing on the fixture select (#10238 (comment))

I am honestly not sure why it's happening. The value on the input ends up being empty by the time the onInput runs even tho it was correct for the onChange (or maybe its the other way around). I didn't spend to much time trying to figure it out since it seemed fine to keep the old behavior.

Let me poke at it...

@jquense
Copy link
Contributor

jquense commented Aug 11, 2017

@sebmarkbage
Copy link
Collaborator Author

So one issue is that targetInst here can be the surrounding <div /> instead of the <select />. https://github.com/sebmarkbage/react/blob/2348742f0baccca745b2f76a2720706a56eafce1/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js#L140

That's because extractEvents gets called first for the inner subtree, and then later for the outer subtree. In the inner subtree, the deepest target instance is the div.

@jquense
Copy link
Contributor

jquense commented Aug 12, 2017

With both events it only seems to break in the case where you change locations onChange. What I see is:

  • onInput fires with the correct value on event.target (the select)
  • Header.handleFixtureChange fires and sets window.location
  • onChange fires with the previous value on event.target (still the select, (WAT?))

At no point though does the select.value descriptor setter get called, so something isn't inadvertently resetting the value in React. it really seems like like the event is firing with the wrong value. This also happens in FF though so it is maybe not some undefined behavior

@sebmarkbage
Copy link
Collaborator Author

sebmarkbage commented Aug 12, 2017

I think the issue that makes the behavior different in the nested case is that this returns the wrong thing and a change event gets fired on the surrounding "div". https://github.com/facebook/react/blob/master/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js#L172

Because targetNode is the "div" and then it tries to do a "restore". But the "restore" works against the native event target so it restores the value.

@sebmarkbage
Copy link
Collaborator Author

This ensures that they're consistent for the nested vs not case: 43e4619

However, this only works because the happen to ignore the restore event properly. The fact that this worked in the normal case seems super fragile to me. Is there another nesting where this breaks? So I'm not super happy with this solution. :/

@sebmarkbage sebmarkbage changed the title Fire onChange Event Even For onInput Events Use the virtual target for change events to avoid restoring controlled state on the real target Aug 12, 2017
@sebmarkbage
Copy link
Collaborator Author

I think I'm going to merge this because this solves this particular issue.

I think we've discovered other issues here. If there is a change listener, higher up in the tree, then it will get invoked as a "change" event. I don't know what kind of consequences that will have.

The failure repros only when two events are fired sequentially, where the second
event is what updates the value.
Ensures that we use the same node to make decisions instead of two like
we do now.

This will cause restore to be called or not called consistently.
@jquense
Copy link
Contributor

jquense commented Aug 12, 2017

That's because extractEvents gets called first for the inner subtree, and then later for the outer subtree. In the inner subtree, the deepest target instance is the div.

ah that is tricky :/ is the underlying problem that the additional change event fires? I was seeing extractEvents called for the select first, then the div. The extractEvents shouldn't fire at all for the div right?

@sebmarkbage
Copy link
Collaborator Author

If someone uses dispatchEvent to dispatch a "change" event on something arbitrary that the browser wouldn't. Such as a custom-element. Should that be picked up by a React onChange listener above it? ¯\_(ツ)_/¯

@sebmarkbage
Copy link
Collaborator Author

sebmarkbage commented Aug 12, 2017

I think the issue behavior change is that this is now always true. https://github.com/sebmarkbage/react/blob/2348742f0baccca745b2f76a2720706a56eafce1/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js#L165

It used to be that we sometimes would fall into the case where this was false for unknown targets.

@sebmarkbage
Copy link
Collaborator Author

Hm. I'm going to change my patch a bit. I don't think we want to pass the virtual target as the event target in the event object. We don't do that elsewhere.

With regard to when to fire this. I think we probably don't want to bubble the event on any blur which I think it what will happen now, right?

We normally pass the native event target to the event object. This ensures
that we do the same thing here.

We still want to schedule a "restore" on the virtual target though since
those are the only nodes known to React.
@sebmarkbage sebmarkbage merged commit 3bc6432 into facebook:master Aug 12, 2017
@sebmarkbage
Copy link
Collaborator Author

There's something off about the way we do bubbling and how we create custom events. We extract events once per subtree so that we can do the bubbling within that subtree. That means that we have to recreate the same event for each subtree. Ideally using the nativeEventTarget as the basis for all decisions so it's consistent. However, we can't do that right now because the change event plugin's extractEvents function is not idempotent because updateValueIfChanged changes the value when observed.

I'm tempted to leave it semi-broken as is now so we don't break it more and then revisit semantics in 17.

The workaround to get sane semantics is to use Portals for now.

sophiebits added a commit to sophiebits/react that referenced this pull request Aug 21, 2017
…ontrolled state on the real target (facebook#10444)"

This reverts the meaningful (src, non-test) part of commit 3bc6432 since we've reverted the commit it depended on in 18083a8. I don't fully understand the negative implications of leaving this unreverted but it sounds like consensus is that it's safer to revert.

I left the new fixture and verified it works correctly in Chrome 62 Mac, as well as the jest tests passing.
sophiebits added a commit that referenced this pull request Aug 22, 2017
…ontrolled state on the real target (#10444)" (#10504)

This reverts the meaningful (src, non-test) part of commit 3bc6432 since we've reverted the commit it depended on in 18083a8. I don't fully understand the negative implications of leaving this unreverted but it sounds like consensus is that it's safer to revert.

I left the new fixture and verified it works correctly in Chrome 62 Mac, as well as the jest tests passing.
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.

4 participants