-
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
[react-interactions] Add Listener API + useEvent hook #17651
Conversation
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 416ee34:
|
23eaa87
to
7759719
Compare
16df9d7
to
72ed512
Compare
This comment has been minimized.
This comment has been minimized.
72ed512
to
54451f6
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
81c934b
to
2f076e6
Compare
This comment has been minimized.
This comment has been minimized.
2f076e6
to
0bec034
Compare
I've redesign and refactored the code in this PR after much delibration and feedback today. Notably: this design is far more low-level and more like how events are added with Here's another example of this looks like now: function Component() {
const divRef = useRef(null)
// Creates a clickEvent that acts like a-kind of
// Event Map. This should always happen in the
// render phase, so we can properly setup the listeners
// unconditionally. Furthermore, we can detect
// this listeners during SSR, and eagerly setup
// the right event listeners on init, to better
// enhance event replaying (for custom events etc).
const clickEvent = useEvent('click', {
// You can leverage the same benefits
// available today on the web, such as
// passive event listeners.
passive: true,
capture: false,
})
useEffect(() => {
const div = divRef.current;
// Set a listener for a given DOM node
clickEvent.setListener(div, nativeEvent => {
console.log(nativeEvent);
});
// You can set a listener to the `window` or
// any other node managed by React. Nodes
// not managed by React will result in
// an error being fired.
clickEvent.setListener(window, nativeEvent => {
console.log(nativeEvent);
});
// You can also delete a listener by passing `null`
clickEvent.setListener(div, null);
// If you have many listeners, you can clear them
// all without having to know of them
clickEvent.clear();
// Note: listeners will automatically be
// destroyed if the component with the hook
// is detatched, or the DOM node that the
// listener is attached to is detatched.
}, [clickEvent])
return <div ref={divRef} />
} |
I don't think this API is sufficient for creating a global gesture system like the responder system in React Native. In that case you might only want to set up one listener for each touch event type on the window to track touches across the entire surface, and to coordinate how the interaction lock is managed across multiple views (accounting for views not managed by react). |
@necolas How would this API be more suffecient for that use-case? Would you mind explaining what is missing? I've also tweaked the API after @TrySound's suggestion to make |
87c7c7d
to
c62180a
Compare
I don't think there's anything wrong with this hooks API or that it necessarily needs (or can be) adjusted to support "global" systems like modality tracking and interaction-locks. They rely on a single coordinator that listens only to "top" level events on the window/document. |
Out of curiosity: is there a chance that setting listeners in |
@devknoll You're right, in some cases you'd want to use |
f2de5cc
to
c73efdb
Compare
7035587
to
c82ae83
Compare
9d4acaf
to
9514f59
Compare
Fix flow fix conflict Add missing flag
9514f59
to
416ee34
Compare
Note: This API is intentionally meant to be a low-level way of creating events and assigning listeners to them. It's meant to be verbose so larger building blocks can be created on top of them.
This PR is an alternative solution and system to that of my other PR: #17508. Specifically, based off feedback internally, I've tried to tackle some of the problems that were brought up with the
createListener
approach in #17508:createListener
largely depended on events being registered in the commit phase, meaning that there would likely be issues around needing to flush more to ensure we register DOM events. The new approach enforces all events are registered unconditionally via hooks in the render phase, mitigating this issue.createListener
allowed for listeners to update and change their properties between renders, which again is problematic for performance and would also require more flushes to ensure we have committed the latest version of each listener.createListener
had a complex diffing process to ensure we stored the latest listeners, but this meant that the process had additional overhead and memory usage – which is no longer the case with this PR.createListener
required listeners to be put on nodes via thelisteners
prop. Furthermore, it required using arrays to combine multiple listeners, which some felt was not idealistic and might be confusing during debugging as to which listeners occurred at which stages. Also, there was general dislike to introducing another internal prop – as it would mean we'd have to first forbidlisteners
and wait for React 17 to introduce these APIs, as they might be used in the wild for other reasons (custom elements).createListener
didn't provide an idiomatic way to conditionally add/remove root events (they're called delegated events in this new PR). With the new approach, there's a streamlined approach to ensure this is easier to do, and by default, no root events can be added unconditionally, which is a code-smell and a good cause of memory leaks/performance issues.Taking the above points into consideration, the design of this new event system aims at bringing the same capabilities as described in #17508 whilst also providing some other nice features, that should allow for bigger event sub-systems to be built on top.
ReactDOM.useEvent
This hook allows for the registration to a native DOM event, similar to that of
addEventListener
on the web.useEvent
takes a given eventtype
and registers it to the DOM then returns an object unique to that event that allows listeners to be attached to their targets in an effect or another event handler. The object provides three different methods to setup and handle listeners:setListener(target: window | element, listener: ?(Event => void))
set a listener to be active for a given DOM node. The node must be a DOM node managed by React or it can be thewindow
node for delegation purposes. If the listener isnull
orundefined
then we remove the given listener for that DOM node orwindow
object.clear()
remove all listenersThe hook takes three arguments:
type
the DOM event to listen tooptions
an optional object allowing for additional properties to be defined on the event listener. The options are:passive
provide an optional flag that tells the listener to register a passive DOM event listener or an active DOM event listenercapture
provide an optional flag that tells the listener callback to fire in the capture phase or the bubble phasepriority
provide an optional Scheduler priority that allows React to correct schedule the listener callback to fire at the correct priority.Note
For propagation, the same rules of
stopPropagation
andstopImmediatePropagation
apply to these event listeners. These methods are actually monkey-patched, as we use the actual native DOM events with this system and API, rather than Synthetic Events.currentTarget
andeventPhase
are also respectfully monkey-patched to co-ordinate and align with the propagation system involved internally within React.Furthermore, all event listeners are passive by default. If is desired to called
event.preventDefault
on an event listener, then the event listener should be made active via thepassive
option.Examples
An example of a basic clickable button:
If you want to listen to events that are delegated to the window, you can do that:
If you wanted to extract the verbosity out of this into a custom hook, then it's possible to do so:
A more complex button that tracks when the button is being pressed with the mouse:
What about the DOM's
element.addEventListener
?In many respects, this low-level API was intentionally designed to be a replacement for the DOM's
addEventListener
. Not only does this new Listener API provide many nice benefits, like auto-recycling listeners on unmount, but it should also help prevent bugs that will likely occur whenaddEventListener
is used in conjunction with Concurrent Mode.For a detailed list of differences, here are just some of the key benefits of the Listener API vs the DOM's
addEventListener
: