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

[Flare] Add useEvent hook implementation #15927

Merged
merged 5 commits into from Jun 21, 2019
Merged

Conversation

trueadm
Copy link
Contributor

@trueadm trueadm commented Jun 19, 2019

This PR adds a new primitive hook to React called useEvent. For now it's behind the React Flare flag and is labelled unstabled_useEvent on the React object. This implementation might have bugs in, as it's not been hooked up and tested with the various event responder modules, like Press, Hover etc. This isn't intended to be a bug-free PR, more it's an initial implementation that should aim to get the wiring right between fibers and the hook.

The principal way of using this, is to leverage existing event components within a component. For example:

import React, {unstable_useEvent} from 'react';
import Press from 'react-events/press';

function MyComponent() {
  unstable_useEvent(Press, {onPress: handlePressFromHook});

  function handlePressFromHook() {
    console.log('hook');
  } 
  function handlePressFromEventComponent() {
    console.log('event component');
  } 
  
  return (
    <Press onPress={handlePressFromEventComponent}>
      <div>Press me</div>
    </Press>
  );
}

Any event components in the same branch of an event component (not above the event component), will fire for the same type of event component. If there are no event components used in the branch of the function component that has the hooks, then the hook props will never fire. So this won't work:

function MyComponent() {
  unstable_useEvent(Press, {onPress: handlePressFromHook});
  
  // You need to use a <Press> somewhere in this branch
  return (
    <div>Press me</div>
  );
}

What is powerful about the hook form of event components, is that they can track events occurring in their tree without needing to worry about propagation rules. All events will propagate to the hook form of these events, which is different to the event component form, where events stop propagation to the same type of event components.

Event responders can opt out of support hooks, for example FocusScope will not support hook form right now, so the responder module has allowEventHooks set to false.

The hook forms thus allow for control of events on children, where their implementation is not fully known or controller by the parent component. The only exception to this, is if an event responder takes global ownership, then no other events components or hooks will fire – this is intentional.

Furthermore, displayName is now no longer on event components and has instead been moved to the responders. This is to allow for hooks to have display names, via the responder.

@sizebot
Copy link

sizebot commented Jun 19, 2019

ReactDOM: size: 0.0%, gzip: 0.0%

Details of bundled changes.

Comparing: e61c9e0...4179acc

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js +0.4% +0.6% 111.93 KB 112.4 KB 35.38 KB 35.58 KB NODE_PROFILING
react-dom-unstable-native-dependencies.development.js 0.0% 0.0% 60.76 KB 60.76 KB 15.85 KB 15.85 KB UMD_DEV
ReactDOM-dev.js +0.6% +0.4% 904.98 KB 910.19 KB 200.84 KB 201.63 KB FB_WWW_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% 0.0% 10.74 KB 10.74 KB 3.68 KB 3.68 KB UMD_PROD
ReactDOMServer-dev.js 0.0% 0.0% 136.3 KB 136.32 KB 34.9 KB 34.91 KB FB_WWW_DEV
react-dom-unstable-fire.development.js +0.5% +0.3% 882.3 KB 887.04 KB 200.71 KB 201.38 KB UMD_DEV
react-dom-unstable-fire.production.min.js 🔺+0.4% 🔺+0.6% 108.51 KB 108.97 KB 35.01 KB 35.23 KB UMD_PROD
react-dom-unstable-fire.profiling.min.js +0.4% +0.5% 111.73 KB 112.2 KB 36.01 KB 36.18 KB UMD_PROFILING
react-dom-unstable-fire.development.js +0.5% +0.3% 876.66 KB 881.4 KB 199.14 KB 199.79 KB NODE_DEV
react-dom-server.node.development.js 0.0% 0.0% 135.56 KB 135.59 KB 35.85 KB 35.86 KB NODE_DEV
react-dom-unstable-fire.production.min.js 🔺+0.4% 🔺+0.6% 108.52 KB 108.99 KB 34.48 KB 34.69 KB NODE_PROD
react-dom-server.node.production.min.js 🔺+0.1% 🔺+0.1% 20.22 KB 20.23 KB 7.57 KB 7.58 KB NODE_PROD
react-dom.development.js +0.5% +0.3% 881.96 KB 886.7 KB 200.57 KB 201.23 KB UMD_DEV
react-dom-unstable-fire.profiling.min.js +0.4% +0.6% 111.95 KB 112.42 KB 35.38 KB 35.59 KB NODE_PROFILING
react-dom-server.browser.development.js 0.0% 0.0% 137.5 KB 137.53 KB 36.24 KB 36.25 KB UMD_DEV
react-dom.production.min.js 🔺+0.4% 🔺+0.6% 108.49 KB 108.96 KB 35 KB 35.22 KB UMD_PROD
ReactFire-dev.js +0.6% +0.4% 904.19 KB 909.4 KB 200.89 KB 201.63 KB FB_WWW_DEV
react-dom-server.browser.production.min.js 🔺+0.1% 🔺+0.1% 19.44 KB 19.45 KB 7.27 KB 7.28 KB UMD_PROD
react-dom.profiling.min.js +0.4% +0.5% 111.72 KB 112.18 KB 36 KB 36.18 KB UMD_PROFILING
ReactFire-prod.js 🔺+1.0% 🔺+0.7% 353.67 KB 357.1 KB 64.65 KB 65.11 KB FB_WWW_PROD
react-dom.development.js +0.5% +0.3% 876.32 KB 881.06 KB 198.99 KB 199.64 KB NODE_DEV
ReactFire-profiling.js +1.0% +0.7% 358.81 KB 362.26 KB 65.66 KB 66.13 KB FB_WWW_PROFILING
react-dom-server.browser.development.js 0.0% 0.0% 133.63 KB 133.66 KB 35.3 KB 35.32 KB NODE_DEV
react-dom.production.min.js 🔺+0.4% 🔺+0.6% 108.5 KB 108.97 KB 34.47 KB 34.68 KB NODE_PROD
react-dom-server.browser.production.min.js 🔺+0.1% 🔺+0.1% 19.37 KB 19.38 KB 7.27 KB 7.28 KB NODE_PROD
ReactDOM-prod.js 🔺+0.9% 🔺+0.7% 365.69 KB 369.13 KB 67.13 KB 67.61 KB FB_WWW_PROD
ReactDOMServer-prod.js 🔺+0.1% 🔺+0.1% 48.33 KB 48.36 KB 11.1 KB 11.12 KB FB_WWW_PROD
ReactDOM-profiling.js +0.9% +0.7% 372.09 KB 375.54 KB 68.35 KB 68.85 KB FB_WWW_PROFILING
react-dom-unstable-native-dependencies.development.js 0.0% 0.0% 60.43 KB 60.43 KB 15.72 KB 15.72 KB NODE_DEV
react-dom-unstable-fizz.node.development.js 0.0% +0.1% 3.88 KB 3.88 KB 1.51 KB 1.51 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% 0.0% 10.48 KB 10.48 KB 3.58 KB 3.58 KB NODE_PROD
react-dom-unstable-fizz.node.production.min.js 0.0% 🔺+0.2% 1.1 KB 1.1 KB 666 B 667 B NODE_PROD
react-dom-test-utils.development.js 0.0% 0.0% 57.6 KB 57.6 KB 15.81 KB 15.81 KB UMD_DEV
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 3.81 KB 3.81 KB 1.53 KB 1.54 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% 🔺+0.1% 1.21 KB 1.21 KB 705 B 706 B UMD_PROD
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 3.64 KB 3.64 KB 1.49 KB 1.49 KB NODE_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% 🔺+0.2% 1.05 KB 1.05 KB 636 B 637 B NODE_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-dev.js +0.5% +0.4% 554.43 KB 557.2 KB 115.73 KB 116.16 KB FB_WWW_DEV
react-art.development.js +0.4% +0.3% 610.94 KB 613.49 KB 133.66 KB 134.09 KB UMD_DEV
react-art.production.min.js 🔺+0.5% 🔺+0.7% 99.41 KB 99.87 KB 30.42 KB 30.65 KB UMD_PROD
react-art.development.js +0.5% +0.4% 541.88 KB 544.44 KB 116.22 KB 116.66 KB NODE_DEV
react-art.production.min.js 🔺+0.7% 🔺+1.2% 64.4 KB 64.87 KB 19.76 KB 19.99 KB NODE_PROD
ReactART-prod.js 🔺+0.8% 🔺+0.9% 209.5 KB 211.18 KB 35.47 KB 35.8 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +0.5% +0.4% 555.56 KB 558.11 KB 119.1 KB 119.53 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.7% 🔺+1.0% 65.71 KB 66.18 KB 20.21 KB 20.42 KB UMD_PROD
ReactShallowRenderer-dev.js +0.1% +0.2% 35.17 KB 35.2 KB 8.79 KB 8.8 KB FB_WWW_DEV
react-test-renderer.development.js +0.5% +0.4% 551.1 KB 553.65 KB 117.97 KB 118.41 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.7% 🔺+1.1% 65.43 KB 65.9 KB 19.97 KB 20.18 KB NODE_PROD
ReactTestRenderer-dev.js +0.5% +0.4% 565.83 KB 568.6 KB 118.29 KB 118.72 KB FB_WWW_DEV
react-test-renderer-shallow.development.js +0.1% +0.1% 42.08 KB 42.11 KB 10.88 KB 10.9 KB UMD_DEV
react-test-renderer-shallow.production.min.js 🔺+0.1% 🔺+0.1% 11.73 KB 11.74 KB 3.58 KB 3.58 KB UMD_PROD
react-test-renderer-shallow.development.js +0.1% +0.1% 36.22 KB 36.25 KB 9.51 KB 9.53 KB NODE_DEV
react-test-renderer-shallow.production.min.js 🔺+0.1% 🔺+0.1% 11.87 KB 11.88 KB 3.7 KB 3.7 KB NODE_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactFabric-dev.js +0.4% +0.3% 677.81 KB 680.55 KB 143.69 KB 144.11 KB RN_FB_DEV
ReactFabric-prod.js 🔺+0.6% 🔺+0.8% 254.03 KB 255.61 KB 43.71 KB 44.06 KB RN_FB_PROD
ReactNativeRenderer-dev.js +0.4% +0.3% 688.96 KB 691.7 KB 146.3 KB 146.71 KB RN_OSS_DEV
ReactFabric-profiling.js +0.6% +0.7% 261.51 KB 263.08 KB 45.16 KB 45.5 KB RN_FB_PROFILING
ReactNativeRenderer-prod.js 🔺+0.6% 🔺+0.8% 261.25 KB 262.83 KB 44.85 KB 45.19 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +0.6% +0.7% 269.13 KB 270.71 KB 46.45 KB 46.79 KB RN_OSS_PROFILING
ReactNativeRenderer-dev.js +0.4% +0.3% 689.05 KB 691.79 KB 146.35 KB 146.77 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+0.6% 🔺+0.8% 261.25 KB 262.83 KB 44.86 KB 45.2 KB RN_FB_PROD
ReactNativeRenderer-profiling.js +0.6% +0.7% 269.12 KB 270.7 KB 46.46 KB 46.8 KB RN_FB_PROFILING
ReactFabric-dev.js +0.4% +0.3% 677.71 KB 680.45 KB 143.65 KB 144.07 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.6% 🔺+0.8% 254.03 KB 255.61 KB 43.7 KB 44.05 KB RN_OSS_PROD
ReactFabric-profiling.js +0.6% +0.7% 261.51 KB 263.09 KB 45.15 KB 45.49 KB RN_OSS_PROFILING

react-events

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-events-hover.development.js +0.4% +1.1% 8.83 KB 8.87 KB 2.14 KB 2.16 KB UMD_DEV
react-events-drag.development.js +0.3% +0.2% 83.76 KB 84 KB 21.39 KB 21.44 KB UMD_DEV
react-events-hover.production.min.js 🔺+0.8% 🔺+1.2% 3.78 KB 3.81 KB 1.39 KB 1.41 KB UMD_PROD
ReactEventsFocus-dev.js +0.5% +0.7% 7.16 KB 7.2 KB 1.86 KB 1.87 KB FB_WWW_DEV
react-events-drag.production.min.js 🔺+0.3% 🔺+0.4% 10.33 KB 10.36 KB 3.88 KB 3.9 KB UMD_PROD
ReactEventsScroll-dev.js +1.0% +1.4% 3.74 KB 3.77 KB 1.2 KB 1.22 KB FB_WWW_DEV
react-events-focus-scope.development.js +0.9% +1.5% 4.23 KB 4.27 KB 1.31 KB 1.33 KB UMD_DEV
react-events.production.min.js 0.0% 🔺+0.2% 682 B 682 B 430 B 431 B UMD_PROD
ReactEventsPress-dev.js +0.2% +0.3% 24.48 KB 24.51 KB 5.59 KB 5.6 KB FB_WWW_DEV
react-events-focus-scope.production.min.js 🔺+1.7% 🔺+1.9% 1.8 KB 1.83 KB 947 B 965 B UMD_PROD
ReactEventsSwipe-dev.js +0.7% +1.0% 6.21 KB 6.25 KB 1.73 KB 1.75 KB FB_WWW_DEV
ReactEventsPress-prod.js -2.4% 0.0% 20.99 KB 20.49 KB 4.27 KB 4.27 KB FB_WWW_PROD
ReactEventsSwipe-prod.js -5.1% 🔺+0.3% 5.85 KB 5.55 KB 1.48 KB 1.48 KB FB_WWW_PROD
react-events-focus-scope.development.js +1.0% +1.4% 4.03 KB 4.07 KB 1.26 KB 1.28 KB NODE_DEV
react-events-focus-scope.production.min.js 🔺+1.9% 🔺+2.3% 1.6 KB 1.63 KB 856 B 876 B NODE_PROD
react-events-focus.development.js +0.5% +0.9% 7.23 KB 7.26 KB 1.88 KB 1.89 KB UMD_DEV
react-events-scroll.development.js +1.0% +1.6% 3.95 KB 3.99 KB 1.25 KB 1.27 KB UMD_DEV
react-events-focus.production.min.js 🔺+1.0% 🔺+1.5% 3.05 KB 3.08 KB 1.17 KB 1.19 KB UMD_PROD
ReactEventsFocusScope-dev.js +0.9% +1.3% 4.01 KB 4.04 KB 1.24 KB 1.25 KB FB_WWW_DEV
react-events-scroll.production.min.js 🔺+1.7% 🔺+2.0% 1.74 KB 1.77 KB 871 B 888 B UMD_PROD
ReactEventsFocusScope-prod.js -3.0% 🔺+0.7% 3.43 KB 3.33 KB 1.06 KB 1.07 KB FB_WWW_PROD
react-events-focus.development.js +0.6% +1.0% 7.04 KB 7.08 KB 1.82 KB 1.84 KB NODE_DEV
react-events-scroll.development.js +1.0% +1.5% 3.76 KB 3.8 KB 1.2 KB 1.21 KB NODE_DEV
react-events-focus.production.min.js 🔺+1.1% 🔺+1.5% 2.87 KB 2.9 KB 1.09 KB 1.11 KB NODE_PROD
react-events-scroll.production.min.js 🔺+2.0% 🔺+2.8% 1.53 KB 1.56 KB 775 B 797 B NODE_PROD
ReactEventsFocus-prod.js -1.9% 🔺+1.0% 6.15 KB 6.03 KB 1.44 KB 1.45 KB FB_WWW_PROD
ReactEventsScroll-prod.js -3.7% 🔺+0.9% 3.22 KB 3.1 KB 991 B 1000 B FB_WWW_PROD
react-events-hover.development.js +0.5% +1.3% 8.64 KB 8.68 KB 2.08 KB 2.11 KB NODE_DEV
react-events-drag.development.js +0.5% +0.7% 7.71 KB 7.75 KB 2.36 KB 2.38 KB NODE_DEV
react-events-hover.production.min.js 🔺+0.8% 🔺+1.6% 3.59 KB 3.62 KB 1.32 KB 1.34 KB NODE_PROD
react-events-drag.production.min.js 🔺+1.0% 🔺+1.1% 3.15 KB 3.18 KB 1.45 KB 1.46 KB NODE_PROD
react-events-press.development.js +0.2% +0.4% 23.66 KB 23.7 KB 5.44 KB 5.46 KB UMD_DEV
react-events-swipe.development.js +0.7% +1.1% 6.24 KB 6.29 KB 1.75 KB 1.77 KB UMD_DEV
react-events-press.production.min.js 🔺+0.4% 🔺+0.4% 8.35 KB 8.38 KB 3.03 KB 3.04 KB UMD_PROD
ReactEventsHover-dev.js +0.4% +1.1% 8.84 KB 8.88 KB 2.14 KB 2.16 KB FB_WWW_DEV
react-events-swipe.production.min.js 🔺+1.2% 🔺+1.5% 2.61 KB 2.64 KB 1.19 KB 1.2 KB UMD_PROD
ReactEventsDrag-dev.js +0.7% +0.9% 6.01 KB 6.05 KB 1.65 KB 1.67 KB FB_WWW_DEV
ReactEventsHover-prod.js -3.2% -0.1% 7.7 KB 7.45 KB 1.79 KB 1.78 KB FB_WWW_PROD
ReactEventsDrag-prod.js -6.9% 🔺+0.6% 5.37 KB 5 KB 1.38 KB 1.39 KB FB_WWW_PROD
react-events-press.development.js +0.2% +0.4% 23.47 KB 23.51 KB 5.39 KB 5.41 KB NODE_DEV
react-events-swipe.development.js +0.7% +1.1% 6.05 KB 6.1 KB 1.71 KB 1.73 KB NODE_DEV
react-events-press.production.min.js 🔺+0.4% 🔺+0.5% 8.16 KB 8.19 KB 2.97 KB 2.99 KB NODE_PROD
react-events-swipe.production.min.js 🔺+1.3% 🔺+1.9% 2.42 KB 2.45 KB 1.12 KB 1.14 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler-reflection.development.js +0.1% +0.1% 19.29 KB 19.3 KB 6.14 KB 6.15 KB NODE_DEV
react-reconciler-reflection.production.min.js 0.0% -0.1% 2.58 KB 2.58 KB 1.13 KB 1.13 KB NODE_PROD
react-reconciler-persistent.development.js +0.5% +0.4% 537.52 KB 540.07 KB 113.66 KB 114.09 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+0.7% 🔺+1.1% 65.66 KB 66.13 KB 19.57 KB 19.78 KB NODE_PROD
react-reconciler.development.js +0.5% +0.4% 539.93 KB 542.48 KB 114.7 KB 115.15 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.7% 🔺+1.1% 65.65 KB 66.12 KB 19.56 KB 19.77 KB NODE_PROD

Generated by 🚫 dangerJS

@TrySound
Copy link
Contributor

Could you provide some use case for hook form over component only form?

@trueadm
Copy link
Contributor Author

trueadm commented Jun 19, 2019

@TrySound How do you mean? You can use the hook form to get events deep in a tree. Event components don't propagate events up the tree to other event components of the same type. Using hooks you can get access to these events that would normally wouldn't propagate.

Validate hooks have decendent event components

Few fixes and displayName changes

Fix more responder bugs

Update error codes
@TrySound
Copy link
Contributor

TrySound commented Jun 19, 2019

So will this allow me to close popup menu on every item press like this?

const Menu = ({ closeMenu, doAction1, doAction2 }) => {
  useEvent(Press, { onPress: closeMenu })
  return <>
    <Press onPress={doAction1}>
      <button>do action 1</button>
     </Press>
    <Press onPress={doAction2}>
      <button>do action 2</button>
    </Press>
  </>
}

@trueadm
Copy link
Contributor Author

trueadm commented Jun 19, 2019

@TrySound Yep, that's right :)

@alazzawimahmoud
Copy link

This will decline the need for lots of custom hooks out there, very promising!

@chengcyber
Copy link

chengcyber commented Jun 20, 2019

Would it support custom event? If so, it brings a total new control flow to react. For now, upper component is notified the changes from descendant components by passing down event handler through props, which leads to hoist some states to upper component. If I understand this pr correctly, would it eliminate the state hoisting technology, in this way, the states keep inside descendant component, while ancestor component can observe them?

@devongovett
Copy link
Contributor

I like the idea here, but the API feels a bit backwards from what we're used to with hooks, so I thought it might be worth pointing out.

All of the existing hooks in React get data from or affect parent components or the component they are called from: never their children. For example, useContext gets data from a parent, and useEffect affects the current component. These new event hooks work the other way: they get data (events) from children. To me, it feels a bit weird to use hooks as the API for event bubbling as opposed to components. It breaks the React model of unidirectional data flow.

It's also a bit confusing that there are two ways of using events: components and hooks.

  • The difference between them and when to use one or the other is unclear at first glance. The only major difference is event propagation as far as I can tell.
  • It's unclear (e.g. in the above example by @TrySound) what the order of events is since the hook is not part of the tree. Does closeMenu run before or after doAction1 or doAction2?
  • It's unclear if event propagation can be controlled in any way. Can doAction1 prevent propagation to closeMenu? Is it possible for the hook to prevent further propagation to parent hooks? There are very legit use cases for this.

Event components with normal event propagation feel like the more natural way. In addition to feeling closer to the way the DOM tree works, the event components are more powerful and less confusing:

  • Event components are a visible part of the rendered tree rather than being an invisible wrapper.
  • You can wrap event components around only one subtree rendered in a component, whereas the hooks would affect the entire component tree.
  • Since they are visibly part of the tree, event components might prevent bugs. For example, if you added a new wrapper to an existing component with a new subtree that shouldn't receive events, the event component would still only wrap the part that should, whereas the hook would now wrap the new part as well. With hooks, you'd need to factor this into two components.

Rewriting @TrySound's example, it could look like this:

const Menu = ({ closeMenu, doAction1, doAction2 }) => {
  return <Press onPress={closeMenu}>
    <Press onPress={doAction1}>
      <button>do action 1</button>
     </Press>
    <Press onPress={doAction2}>
      <button>do action 2</button>
    </Press>
  </Press>
}

Assuming the events passed to those handlers had a normal stopPropagation method, they could easily have all of the power I described earlier without a lot of the confusion.

I'd be interested to hear more of the reasoning around this, and why simply adding event propagation support to event components was not the way forward. What does this enable that simply using components does not?

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Jun 20, 2019

There are a bunch of reasons why these primitives are worth while. Exploring.

While the implementation uses bubbling you can also implement this using context by wrapping a context at each level. Eg think passing a dispatcher down. Another way to think about states that this could implement is reading a path like if this component is on a path towards the hovered target node. That’s observing a state similarly to useContext.

What hooks doesn’t do is add/change behavior of the tree and this doesn’t neither. It just observes it. That’s also an important distinction from how normal bubbling works. It actually changes the behavior of touch events and such.

There are a number of reasons we want this for complex cases but to me, the most compelling in the simplest ones.

Doing a :hover or :focus-children pseudo-selector is really simple in CSS.

Doing the same thing with rich components is a pita.

This primitive let us implement simple effects with really small syntax.

function ButtonWithHoverState() {
let isHovered = useHover();
return <CustomButton>
  <Label />
  {isHovered ? <Overlay /> : null}
</CustomButton>;
}

This syntax is already a bit much. Any solution adding refs or explicit wiring or callbacks to this is way too much.

A render prop could also do this but feels like going backwards with regard to nesting and syntax.

We also need to separate the conflation of observing a target and making something a target. That’s one of the thing that has really broken when the web went from precise inputs like mouse to touch. Touch events on a parent changes what can be taped, the hit area. Listening to a focus event doesn’t. You have to use tabIndex to make it a target. Somehow we need to distinguish these cases and hooks vs components seems like a reasonable way to make this split explicit but also gives us the nice syntax short hand above.

@sebmarkbage
Copy link
Collaborator

Another thing that stopPropagation have to deal with is what to do for server rendering. Especially for partial hydration where parents might be hydrated but not all children. The programmatic api needs to become way more declarative anyway. So the existing DOM approach is likely not going to be sufficient regardless.

@devongovett
Copy link
Contributor

This doesn't seem too different to me, but it is much clearer about:

  1. Which elements it applies to.
  2. When it will cause a component to re-render (where the state lives).
function ButtonWithHoverState() {
  let [isHovered, setHovered] = useState(false);
  return <Hover onHoverChange={setHovered}>
    <CustomButton>
      <Label />
      {isHovered ? <Overlay /> : null}
    </CustomButton>
  </Hover>;
}

Not sure what you meant about stopPropagation on the server. How would user events like press and hover fire during server rendering?

@necolas
Copy link
Contributor

necolas commented Jun 20, 2019

These hooks are complementary to, and must be used with event components in the tree. Many common scenarios, like the ones we're supporting internally, do not need the hooks at all.

The issue with partial hydration of server rendered HTML is related to replaying events and the difficulties caused by imperative calls to preventDefault and stopPropagation in event handlers.

This doesn't seem too different to me

In Sebastian's example the <Hover> component is used within the custom button component, which doesn't have to export props for events, and doesn't require a wrapping component

@mindplay-dk
Copy link

Might be a silly question, but why is it an element? It doesn't emit an actual DOM element, it just attaches handlers to the child(ren), right?

@trueadm
Copy link
Contributor Author

trueadm commented Jun 20, 2019

@mindplay-dk

Might be a silly question, but why is it an element? It doesn't emit an actual DOM element, it just attaches handlers to the child(ren), right?

These are only React elements, they're not DOM nodes. In fact, that's what is great about this new event system – you can encapsulate some forms of events without needing to associate them to a container. This is because we don't use the DOM node tree to handle event propagation, but rather the internal React fiber tree. This also allows us to add custom events without needing to worry about conflicting with native events that exist already or might get added in the future.

@devongovett

I originally had the same reservations about this. After working with this for a while and integrating this event system into FB5, it occurred to me that how DOM events work very differently to how they are handled in this new event system – and for good reason too. You kind of need to forget how events worked before and have an open mind with in respect to how they work with React Flare. Very much like when React came around, and people had reservations about some of the patterns it introduced, some of that applies with this new declarative event API too.

Event hooks aren't really anything super special, they're actually very like events components. For example take this:

function MyComponent() {
  useEvent(Press)
  return (
    <Press>
      <Press>
  )
}

When this component and it's sub-tree gets reconciled by React, it actually forms the following ordering for when a "press" event comes in:

<Press> -> <Press> -> Press Hook

The important difference is that event components like <Press>, never propagate events to other event components of the same type, so the middle <Press> will never receive any "press" events. This is to ensure the encapsulation is kept localized within an event component's children. React Flare doesn't allow users to do event.stopPropagation() either, instead everything must be declaratively handled component composition using props. If you take your example above:

function ButtonWithHoverState() {
  let [isHovered, setHovered] = useState(false);
  return <Hover onHoverChange={setHovered}>
    <CustomButton>
      <Label />
      {isHovered ? <Overlay /> : null}
    </CustomButton>
  </Hover>;
}

You're right this looks as good and also works fine! Then you give it to product engineers and then 30 minutes later they come back to you and say "it doens't work"! Let's see what they've done:

function CustomButton() {
  const [hoverStart, hoverEnd] = useAppLoggingMetrics();
  return <Hover onHoverStart={hoverStart} onHoverEnd={hoverEnd}>
    <CustomButtonInternals />
  </Hover>
}

function ButtonWithHoverState() {
  let [isHovered, setHovered] = useState(false);
  return <Hover onHoverChange={setHovered}>
    <CustomButton>
      <Label />
      {isHovered ? <Overlay /> : null}
    </CustomButton>
  </Hover>;
}

Ah, they've also used <Hover> in their CustomButton component, which means our <Hover> event component will never get any events anymore because event components don't propagate their events to other event components of the same type. One possible way around this would be to invalidate our event component design rule and make some event components optionally propagate.

We looked into this and found it actually led to subtle bugs and complications when it comes to correctly knowing the "hit target" among other things. For example, <Press> press event objects tell the user what the target is, but this is actually currentTarget, meaning it would be break:

<Press>
  <div> // b) the target would be this <div>
    <Press>
      <div> // a) I press this <div>

Press needs to know the correct target to apply things like hit retention boundaries and responder regions, plus hit target slop regions (the DOM doesn't have any of this stuff natively today).

With event hooks, because we only associate the hook with the closest event target to the actual press target, this isn't an issue:

<Hook Press>
  <Press>
    <div> // b) I then press this <div> and this is now the target.
      <Press>
        <div> // a) I press this <div> and this is the target.

I also want to address the old way of thinking at this point too: why not just ditch the declarative event system and go back to providing callbacks with an event object that allows for event.stopPropagation() and event.preventDefault(). I'll focus on stopPropagation here, but much of this applies to preventDefault too.

If you provide a way of handling stopPropagation like the old React event system, then you'll run into issues that lead to difficult to find bugs and edge-cases between event components. This often leads to developers resorting to sequencing events to hack around issues or using the capture phase. Then they might run into more issues with stopPropagation in the capture phase, then find that they need to use stopImmediatePropagation. Unknowingly, they've just broken some other part of the app by accident, because these are all side-effects that can be unpredictable in large apps.

Ignoring the error-prone ways, ultimately you're still using imperative control flow in a declarative UI. Declaratively defining events and letting React and your event component responders handle they work is a much better world we should be moving to.

Now the other benefits a declarative event system provides: it makes it possible for React to replay events that occur before client-side hydration has occured. With a declarative event composition, you can see in dev tools what is happening visually from the UI, you can see intent quickly and find issues/bugs with build-time tooling too. Not to mention, a future optimizing compiler can take the declarative information and optimize much of this away one day – something that is not possible with imperative spaghetti code.

How do you declaratively allow the browser defaults with this event system or stop propagation to other imperative event systems? We provide escape hatches in a declarative form here:

// You can declaratively tell Press that you don't want it to prevent the <a> link loading natively
<Press preventDefault={false}><a href="http://example.com /></Press>
// You can declaratively tell Press that you want to block propagation to other event systems outside of React Flare.
<Press stopPropagation={true}><a href="http://example.com /></Press>

Taking all this into hand, we felt that we need a declarative event system that has the right default primitives, we were just missing another form of event encapsulation. To be exact, we felt that there should be two forms of encapsulation:

  • localized within event components (as they are already today)
  • and localized within function component sub-trees (something we are missing)

How do you apply encapsulation to a function component sub-tree? The obvious is to use hooks, as that would allow us to "hook" the fiber representing the function component to the event responder in React's event system. Now, you have a way of "listening" to events of a specific responder, constrained by a component sub-tree. We then needed to constrain the hook based approach so that it only worked with specific target event components; otherwise you'd have a scenario that would mean that events were not explicitly listened to and it would be a nightmare to debug and control. So event hooks take an event responder and correspond to the relevant event components inside the sub-tree where the hook was defined.

In summary:

  • this was a requirement internally where we wanted to be able to catch "press" events from nested <Press> event components.
  • we wanted to expose a way for engineers to wrap up powerful event control in the form of custom hooks.
  • we needed this system to fully work with React Native and server-side rendering hydration (event replaying before concurrent hydration has hydrated a sub-tree).

@benjamingr
Copy link

Hey @trueadm nice reply. I am very worried moving further away from native events and how the DOM works because writing automation for React is already super hard as it is because of how different the event system is from native events.

The DOM already has two mechanisms to deal with the hover issue you mentioned that allow overriding the default (which is not stopping propagation):

  • Some events (like scroll) don't bubble. Some events contain bubbling and non-bubbling alternatives (like blur and focusout).
  • You can capture events (an onHoverCapture) and the component you wrap cannot catch them before you.

I like the idea and the exploration and I think the syntax is interesting but this looks like a shift further away from native events. This breaks two major things:

  • The Chrome devtools tooling doesn't work with React particularly well, for example the event listeners tab shows the event delegation and EventPlugin arch. Moreoever it shows it for the "wrong" events (like how mouseenter listens to mouseover and mouseout).
  • Accessibility tools for people who have a hard time using a computer the "traditional" way have to pick up on this.

Not to mention the fact react renders asynchronously and may handle events asynchronously means that establishing any causality for why something happened for debugging is significantly harder.

I am biased because I have more experience building platforms and tools than apps and I am just one person and my opinion isn't any more valid than others' - I am just concerned about these issues.

@gaearon
Copy link
Collaborator

gaearon commented Jun 20, 2019

Accessibility tools for people who have a hard time using a computer the "traditional" way have to pick up on this.

Note that the intent is quite the opposite — all of these APIs are designed with extra effort to make accessible experiences the default.

These are valid concerns. At this point it's an experimental, completely additive (and unstable) API and React isn't "moving away" from anything. We can have a more informed broader discussion with more context and explanations after we can experiment with this in real products. For now it's a bit difficult to give all context (this is just in a very raw experimental stage), and I'm worried that you might be implying the communication on this PR is all we'll do, or that things are somehow set in stone. That's not the case.

@benjamingr
Copy link

@gaearon oh, I just expressed worry regarding this API and how it interacts with automation and accessibility tools.

I am not saying "don't do it". This is more of a "this is a cool idea and sounds like a fun experiment, here are my concerns".

I am sorry it came as if I am opposed to this, I will communicate more clearly in the future in these sort of issues when I engage. I would never intentionally oppose to a feature without even trying it first and playing with the code (that would be mean and unproductive of me IMO) .

Anyway as always thanks for clarifying things and taking the time to emphasise this is still not set in stone. I am all for experimentation and doing cool things. I hope we can discuss the concerns I've raised (here or elsewhere) in a constructive way that does not attack or harm the experimentation work and effort.

@trueadm
Copy link
Contributor Author

trueadm commented Jun 20, 2019

@benjamingr You made some good points, I'll try and address them accordingly:

  • It's true that debugging with Chrome inspector is harder, but to be honest it's a quite terrible. That's why we have React Dev Tools. There's a new Dev Tools coming in the future that has full support for events that is going to be really appealing. We also plan on surfacing some additional features that will make event debugging much easier than before.
  • We're planning on building a bespoke testing library that works great with this system. Rather than writing tests where you imperatively call dispatchEvent over and over, you'll be able to declaratively define an "interaction". This will then apply the w3c spec for what events would apply (mouse, touch, pen etc built in), so you get a really good testing interface. We need this internally anyway, as the dispatchEvent approach with JSDOM just doesn't work right.
  • The new event responders have full control on how events are dispatched in terms of batching and compatibility with concurrent rendering. They also have full control over browser passive events. This isn't available in current event system.
  • Accessibility is baked into this project. Everything we're doing has a strong emphasis on ensuring screen readers and interaction tooling (Voice Over on iOS for example) is taken into consideration both now and later on in the project.

As @gaearon said, nothing is set in stone. We may change how these features work at anytime, based off internal findings. That doesn't mean we can't discuss pain points now though – it's great to have community feedback on early ideas!

@benjamingr
Copy link

benjamingr commented Jun 20, 2019

@trueadm thanks for the thoughtful reply, it is good to learn that you are taking automation and accessibility seriously and are testing them.

There's a new Dev Tools coming in the future that has full support for events that is going to be really appealing.

Would it be possible to integrate into them in production builds? As far as I know at the moment React and React DevTools find each-other by a global variable which might not exist and usually doesn't in production.

A lot of time issues reproduce only in certain environment or builds and people resort into using the native devtools for debugging. I agree that the React DevTools provide a lot of information missing from the Chrome DevTools but moving even further away from native events means that the Chrome tools (that are always available) become less useful.

I think this can be resolved technologically by making React easier to find though and is not a strong argument against diverging further from native events.

We're planning on building a bespoke testing library that works great with this system.

Yes please! 🙇 I would prefer it if this library was as low-level as possible where you provide basic primitives (find this component from an html element, fire a react event on it). This would absolutely alleviate my concern if it worked and kept some sort of ABI between React versions.

A lot of us are doing grey and black box testing which would need access to this library. I think that if the person writing the test and the person writing the app are different people then a different event system makes things harder. Tools with money (like Testim - my employer) have no issue dealing with this but tools that are much smaller and not backed by VC funds have a hard time with these issues.

Just try writing a cross-browser black-box test for Draft.js :]

The new event responders have full control on how events are dispatched in terms of batching and compatibility with concurrent rendering. They also have full control over browser passive events. This isn't available in current event system.

That's a good point. I am excited by the premise of React being able to make event handlers passive automatically which could help with a bunch of common footguns.

Accessibility is baked into this project

I will try checking this out and ask a few friends who use assistive technologies to use the web to give feedback and I'll report if they say anything interesting.

Thanks for caring about this. I would really appreciate a way (for accessibility tooling) to figure out what caused what change (like synchronous native code does with a MutationObserver) to establish causality across usage for example:

  • "Would you like to discard the current changes?"

vs.

  • "Modal, 'Would you like to discard the current changes?', triggered by clicking on Button 'Reset'"

It is an example of something that is possible with regular web technologies but is harder with React. This is not something Flare needs to fix and I do not believe Flare makes this harder to do but it would be great if we had accessibility tooling baked into the new devtools you are working on.

(This is just my opinion which is biased having worked with people who use assistive technologies a lot, I am not an expert in accessibility and it is possible this sort of feature isn't needed by most of the user base and does not need to be in 'core' react).

@gaearon
Copy link
Collaborator

gaearon commented Jun 20, 2019

Would it be possible to integrate into them in production builds? As far as I know at the moment React and React DevTools find each-other by a global variable which might not exist and usually doesn't in production.

This doesn't sound accurate. React DevTools works in production, and ReactDOM connects to it regardless.

@benjamingr

This comment has been minimized.

@trueadm
Copy link
Contributor Author

trueadm commented Jun 20, 2019

@benjamingr

I think this can be resolved technologically by making React easier to find though and is not a strong argument against diverging further from native events.

This new event system is a big departure from thinking of things in terms of "native events". Instead of thinking about handling events as "let me known when someone does onMouseDown", it's about the interaction and intent.

Event components like <Press> listen to a dozen native events to provide a single onPress consistently, there's no way it would be possible to provide the power of React Flare if we just passed native events to users. We're mostly leveraging Pointer Events too, so we can handle multi-touch across multiple input devices. It's awesome being able to have native-like feedback and trusting <Press> to work the same regardless of platform or input device (touch, pen, trackpad, keyboard and mouse).

When dealing with focus and modals, you'd use <FocusScope> which can contain the focus to its children and handle controlling and managing how focus works. When dealing with forms and inputs, we'll probably introduce <FormScope> that can really impact how form handling works today. These are all made possible by moving more into higher-level events (or interactions) than moving into lower-level events.

Lastly, these event modules are in user-land, thus you only pay the cost for their implementation if you actually use them in your bundle. They can also be bundle split, to reduce impact on initial render. This would not have been possible had we moved more towards a low-level architecture – you'd end up repeating the same thing we're attempting to do in your components (as everyone seems to do) without catching all the edge-cases and caveats that we've spend months finding and fixing. The aim is to cover all your bases so you can build better UIs without React and the DOM getting in your way.

@devongovett
Copy link
Contributor

@trueadm FWIW, I am totally in favor of the new event system. I love the work you've done on the event components so far, and I think they are going to improve the overall UX of all react apps in a major way. ❤️

I am still slightly concerned about the hooks API specifically though, and also want to ensure that usecases previously covered by the ability to stop propagation are properly covered by this new event model. I think many of my points about the clarity of hooks as opposed to components in my original comment still stand, in terms of API ergonomics. Would love to hear your thoughts on them.

In terms of propagation, I think there are some use cases that might be challenging to implement without some kind of control over propagation. I am totally aware that the current imperative methods have their challenges, so by no means do I think that is the right API necessarily. I do think the power they allow is necessary to expose though.

Imagine a confirmation dialog with a button, that when clicked, performs some validation. If the validation is successful, then the dialog closes, otherwise it remains open and displays an error. Assume for the sake of argument that this validation is too expensive to perform while the user is interacting with the content of the dialog. Originally, this dialog was implemented without validation by having <Press> responders wrapping the cancel and confirm buttons, with an event hook to handle closing the dialog on both actions at the dialog component level.

When validation is added which could prevent the dialog from closing, we would need some way to prevent the event from propagating to the hook press handler. With no way to do that, we would now have to refactor the dialog to not use the hook form at all, and instead pass a prop down to each button, and manually chain the close handler with the individual cancel and confirmation handlers. If this dialog component were a generic component implemented by a third party for example, we would have no way to do this and would also need to reimplement the entire dialog component in order to make this happen.

Another issue would arise if you added a new interactive element to the dialog later on, which also happened to use a <Press> event responder internally. Suddenly, the dialog would start closing when you interacted with this element. You'd need to refactor it again in order to not use the hook form, and instead pass props down explicitly to the buttons.

Along these lines, I'm struggling to find an example where using the hook form would not cause problems further down the road, especially if you don't control all of the components in the tree. I think it might be safer to just manually pass props down to the event components further down the tree so that at some point in the middle you can "stop propagation" by not calling the parent's callback. This is obviously very tedious though, so I think it would be good to find an API to express this in a better way.

Finally, coupling the hooks to event components further down the tree could have extensibility problems. Imagine I'm using a third party component, and I want to make something happen when a user hovers over a particular child element. Since I cannot modify the code to insert a <Hover> event responder, this is impossible. With DOM event propagation, I could simply handle events on a parent and check the event's target property to determine what I need to do. With this new model, I'd need to contact the developers of whatever component I was trying to extend, or reimplement it myself.

@trueadm
Copy link
Contributor Author

trueadm commented Jun 20, 2019

@devongovett These are good points, but ultimately it comes down to use-cases. Using the hook event form isn't meant to replace event components, they're meant to complement one-another. In the use case you have above, you'd never use the Press hook to close the dialog box – in fact, I think people will rarely use the hooks in conditional cases like this (with <Press> anyway).

There are a few use-cases in general with events:

  • This event needs to propagate for an action that doesn't disrupt my children. i.e. styling, logging, visual indications. This is a great use-case for hooks
  • This event needs to propagate and has a disruptive action. i.e. close dialogs, route to a new page, cancel an action etc. This is a bad use-case for hooks, ideally these should be put into component context, state or props.

That's not to say there are cases in the future where we might expand on the second pointer. One area of research is by using another new API or concept that allows data passing to be explicit and declarative, whilst keeping it within the bounds of containment. Watch this space, as I'm sure we'll come up with something that works.

Copy link
Contributor

@necolas necolas left a comment

Choose a reason for hiding this comment

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

flare-related change lgtm

@@ -267,8 +270,11 @@ function FiberNode(
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.contextDependencies = null;
this.events = null;
this.dependencies = {
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 create this object lazily? Most fibers will not need it.

@acdlite
Copy link
Collaborator

acdlite commented Jun 21, 2019

I'll follow up on creating the dependency object lazily. Merging to unblock.

@acdlite acdlite merged commit 720db4c into facebook:master Jun 21, 2019
acdlite added a commit to acdlite/react that referenced this pull request Jun 21, 2019
Most fibers do not have events or context, so we save memory lazily
initializing this container node.

Follow-up from facebook#15927
acdlite added a commit to acdlite/react that referenced this pull request Jun 21, 2019
Most fibers do not have events or context, so we save memory lazily
initializing this container node.

Follow-up from facebook#15927
acdlite added a commit that referenced this pull request Jun 21, 2019
Most fibers do not have events or context, so we save memory lazily
initializing this container node.

Follow-up from #15927
rickhanlonii pushed a commit to rickhanlonii/react that referenced this pull request Jun 25, 2019
* [Flare] Add useEvent hook implementation

Validate hooks have decendent event components

Few fixes and displayName changes

Fix more responder bugs

Update error codes

* Add another test

* Address feedback
rickhanlonii pushed a commit to rickhanlonii/react that referenced this pull request Jun 25, 2019
Most fibers do not have events or context, so we save memory lazily
initializing this container node.

Follow-up from facebook#15927
NMinhNguyen referenced this pull request in enzymejs/react-shallow-renderer Jan 29, 2020
* [Flare] Add useEvent hook implementation

Validate hooks have decendent event components

Few fixes and displayName changes

Fix more responder bugs

Update error codes

* Add another test

* Address feedback
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.

None yet