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

Always use event capturing. Remove media events. #12919

Closed
wants to merge 3 commits into from

Conversation

nhunzaker
Copy link
Contributor

@nhunzaker nhunzaker commented May 28, 2018

What

I finally had a chance to circle back on what I learned investigating event listener attachment.

In short, we distinguished between captured and bubbled events, however I believe this is not necessary as of IE9+. Captured events do not appear to exhibit the same edge cases as bubbled events, allowing us to simplify event code.

Why do we want to do this?

I think we can probably drastically reduce the code for eagerly attaching listeners for events that do not bubble. So far, all events that do not bubble appear to capture on parent elements.

Longer term, I wonder if we can remove the .bind() when attaching listeners, and maybe even remove top level types.

More info

This commit changes event subscription such that all events are attached with capturing. This prevents the need to eagerly attach media event listeners to video, audio, and source tags, and simplifies the event subscription process.

I think we can probably extend this to more event types, maybe most/all eager event attachments, but that will require substantial testing. Until that time, this commit lays the foundation for that work by demonstrating that capturing is applicable in cases where bubbling does not occur.

Test plan

  1. Verify fixtures on https://react-capture-all-events.surge.sh/media-events
  2. Verify fixtures on https://react-capture-all-events.surge.sh/input-change-events
  3. Verify fixtures on https://react-capture-all-events.surge.sh/pointer-events

I believe these to be the key event types to test, however this change definitely affects all event listeners.

I've tested so far in:

  • Internet Explorer: 9, 11
  • Edge 16
  • Firefox: 49, 60
  • Chrome 47, 66
  • Safari 9, 11.1


expect(onError).toHaveBeenCalledTimes(1);
} finally {
container.remove();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Worried this might be a breaking change, but the events only appear to capture with JSDOM if the element is attached to the document. Any ideas?

@gaearon
Copy link
Collaborator

gaearon commented May 28, 2018

@sebmarkbage Do you think this could cause issues at FB with ordering of listeners? I don't remember what you bumped into last time we tried to change something there.

@pull-bot
Copy link

pull-bot commented May 28, 2018

ReactDOM: size: -0.6%, gzip: -0.5%

Details of bundled changes.

Comparing: c5a733e...fec5dd3

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js -0.4% -0.3% 626.32 KB 623.88 KB 145.82 KB 145.38 KB UMD_DEV
react-dom.production.min.js -0.6% -0.5% 94.16 KB 93.59 KB 30.5 KB 30.34 KB UMD_PROD
react-dom.development.js -0.4% -0.3% 610.68 KB 608.25 KB 141.81 KB 141.4 KB NODE_DEV
react-dom.production.min.js -0.6% -0.6% 92.66 KB 92.09 KB 29.48 KB 29.3 KB NODE_PROD
react-dom-test-utils.development.js -0.5% -0.8% 45.27 KB 45.04 KB 12.47 KB 12.37 KB UMD_DEV
react-dom-test-utils.development.js -0.6% -0.9% 40.13 KB 39.9 KB 11.04 KB 10.94 KB NODE_DEV
ReactDOM-dev.js -0.4% -0.3% 620.2 KB 617.63 KB 141.21 KB 140.72 KB FB_WWW_DEV
ReactDOM-prod.js -0.6% -0.5% 269.19 KB 267.7 KB 50.57 KB 50.3 KB FB_WWW_PROD
ReactTestUtils-dev.js -0.5% -0.9% 41.52 KB 41.29 KB 11.24 KB 11.14 KB FB_WWW_DEV

Generated by 🚫 dangerJS

@philipp-spiess
Copy link
Contributor

There is a subtle behavior change when you use react events in combination with native document events with this change. Let me outline the issue:

Right now, when you have code like this:

class extends Component {
  componentDidMount() {
    document.addEventListener('click', () => console.log('document bubble'))
    document.addEventListener('click', () => console.log('document capture'), true)
  }
  render() {
    return <div 
      onClick={() => console.log('element bubble')}
      onClickCapture={() => console.log('element capture')} />;
  }
}

A click on the element will fire in the order you would expect (jsfiddle):

document capture
element capture
element bubble
document bubble

This happens because React will add the event listener to the document before the componentDidMount hook run and thus the bubble listener in componentDidMount will fire after the react listener (although both technically listen at the same node).

The same pattern will behave unexpectedly after the change since the now the capture listener that was added first, in our case the once by react, will be fired first. This would probably result in the following order:

element capture
element bubble
document capture
document bubble

We do have code in PSPDFKit for Web that adds document listener in the capture phase which might be affected (Usually doing this to simulate a pointer capture since we use mouse and touch events instead. React got support for pointer events only as of recently 🙈). I'm sure we can work around that but I just wanted to point that out.

Notes:

@gaearon
Copy link
Collaborator

gaearon commented May 28, 2018

It seems like we should just do #2043. It comes up pretty often and is impactful to fix.

@gaearon
Copy link
Collaborator

gaearon commented May 28, 2018

@philipp-spiess Want to take a stab at it?

@philipp-spiess
Copy link
Contributor

@gaearon Sounds good 👍

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented May 28, 2018

Fair warning. #2043 is going to be very risky and take a lot of work, which might include many back and forth after breaking FB, unless you have your own very large code base that can flush out issues.

The polyfills we use for onSelect etc. relies on it being global AFAIK and doing it differently might expose subtle breakages.

I could imagine that it won't actually make it to a release until we do 17.0 for that reason.

@philipp-spiess
Copy link
Contributor

@sebmarkbage Thanks for pointing that out. I looked a bit into the whole topics and there seem to be a lot of pitfalls - That's probably also the reason why this did not land yet despite having a lot of work put into.

One immediate thought I have is if #2043 would even be compatible with this PR. When we listen in the capture phase, the event order for nested react contexts will appear inverted. Pretty much what was pointed out for focus events here will happen for every event.

@gaearon
Copy link
Collaborator

gaearon commented May 28, 2018

If we do #2043 we’ll need to start it behind a feature flag so we can gradually test it and switch over in 17.

@nhunzaker
Copy link
Contributor Author

I don't have much to contribute other than to thank everyone for pointing out the issues with this approach. I'll dig into this again with those in mind.

This commit changes event subscription such that all events are
attached with capturing. This prevents the need to eagerly attach
media event listeners to video, audio, and source tags, and simplifies
the event subscription process.

I think we can probably extend this to more event types; requiring
much more testing. Until that time, this commit lays the foundation
for that work by demonstrating that capturing is applicable in cases
where bubbling does not occur.
@nhunzaker
Copy link
Contributor Author

@philipp-spiess Added your test case (and it fails exactly as you've laid out).

Just for fun, I did a quick test to see what would happen if all event listeners were attached to the root container element:

nhunzaker/react@nh-remove-media-events...nhunzaker:nh-root-attachment-test

This change allows the test to pass, but leaves 11 test failures (neat that it's only 11 though):

  • ReactDOMEventListener › should dispatch events from outside React tree
  • ReactDOMEventListener › Propagation › should propagate events one level down
  • ReactDOMEventListener › Propagation › should propagate events two levels down
  • ReactDOMEventListener › Propagation › should batch between handlers from different roots
  • ReactDOMEventListener › should not fire form events twice
  • SimpleEventPlugin › iOS bubbling click fix › does not add a local click to interactive elements
  • SimpleEventPlugin › iOS bubbling click fix › adds a local click listener to non-interactive elements
  • SelectEventPlugin › does not get confused when dependent events are registered independently
  • SelectEventPlugin › should fire onSelect when a listener is present
  • ReactTreeTraversal › Enter leave traversal › should enter from the window
  • ReactTreeTraversal › Enter leave traversal › should enter from the window to the shallowest

It's possible that this has been discussed before, but it looks like we still need to attach certain events to the owner document for things like enter/leave and the onSelect polyfill.

@philipp-spiess
Copy link
Contributor

@nhunzaker Thanks for adding the test case 🎉

I think #8117 was the latest attempt to implement per-root-container listening if you need some context 🙂 Besides the issues with SelectEventPlugin (which are non-trivial on their own) I'm concerned about the comments regarding the bubble order for nested React instances.

Let’s have a look at an example of one react app (“outer”) that mounts another react app (“inner”): https://codesandbox.io/s/vy9oywmjl3

Right now, the events will be dispatched in this order:

inner capture
inner bubble
outer capture
outer bubble

Which, of course, is unexpected since capture events must fire before bubble events. Now with the changes outlined in #8117, that behavior would still be present. The reason here lies deeply in the fact that we only listen for one top level event (may it be in the bubble phase like on master or in the capture phase) and use that event to simulate both the capture and bubble phase.

The order we see right now (the one I outlined above) is a good compromise since I’m sure that bubble listeners are much more common than capture listeners and we see the inner bubble occurs before the outer bubble which is expected for the bubble phase.

Now if we would naively listen for top level events on the root container element in the bubble phase, we’d see the same order since the inner container will receive the bubble event first and dispatch both phases before the outer container gets a chance.

If we would combine this with your PR, the order would be inverted since the outer container would be the first to receive the event, resulting in:

outer capture
outer bubble
inner capture
inner bubble

Both orders are wrong - although I’d say the latter would be an issue for more people. I have two ideas how that can be fixed but they're huge changes:

  1. We add both a capture and a bubble listener for each top level type and only dispatch the according phase. This would be a big since every event plugin accepts only one call per event. Not to mention potential performance implications.
  2. We get rid of all of that and start to add event listeners on the individual nodes in the phase they’re registered… 🙃

@nhunzaker
Copy link
Contributor Author

Sounds like a universal change to capture at the top has a lot of issues :)

We get rid of all of that and start to add event listeners on the individual nodes in the phase they’re registered…

I experimented with that a bit, motivated to fix some rendering deopts with scroll events (#9333). The challenge I'm having with the current event system is that I need some way to dedupe events. Otherwise React sends out a new synthetic event every step of a bubble (failing this test).

I wonder if a good next step would be to outline a more formal description of how this currently works. From that, we could create a test suite that verifies those rules. Maybe a test suite is all we need. We'd probably need to add more to it over time as we uncover unanticipated changes in behavior.

We could also start to compile a list of use cases React's event system doesn't support. I think one of the reasons there is concern attaching all events locally is performance. But maybe it's not so much slower, or there's some middle-ground that gives us a good balance of performance, behavior and simplicity. Right now it's hard for me to gauge the cost/benefit of the change within the limited context of my own use-cases.

@facebook-github-bot
Copy link

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign up at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need the corporate CLA signed.

If you have received this in error or have any questions, please contact us at cla@fb.com. Thanks!

@facebook-github-bot
Copy link

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@nhunzaker
Copy link
Contributor Author

This PR was useful to uncover a lot of design challenges, but I think it's unrealistic to move forward with.

@nhunzaker nhunzaker closed this Aug 23, 2018
@philipp-spiess
Copy link
Contributor

@nhunzaker Should we extract some of the test cases that we've identified in here and merge it upstream? Maybe to document the expectations that we have about the event system - Does not mean we never change them 🙂

philipp-spiess added a commit to philipp-spiess/react that referenced this pull request Sep 3, 2018
This is documenting the current order in which events are dispatched
when interacting with native document listeners and other React apps.

For more context, check out facebook#12919.
philipp-spiess added a commit to philipp-spiess/react that referenced this pull request Sep 3, 2018
This is documenting the current order in which events are dispatched
when interacting with native document listeners and other React apps.

For more context, check out facebook#12919.
philipp-spiess added a commit to philipp-spiess/react that referenced this pull request Sep 3, 2018
This is documenting the current order in which events are dispatched
when interacting with native document listeners and other React apps.

For more context, check out facebook#12919.
philipp-spiess added a commit that referenced this pull request Sep 3, 2018
This is documenting the current order in which events are dispatched
when interacting with native document listeners and other React apps.

For more context, check out #12919.
jetoneza pushed a commit to jetoneza/react that referenced this pull request Jan 23, 2019
This is documenting the current order in which events are dispatched
when interacting with native document listeners and other React apps.

For more context, check out facebook#12919.
@necolas necolas mentioned this pull request May 28, 2019
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.

6 participants