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
Conversation
ReactDOM: size: 0.0%, gzip: 0.0% Details of bundled changes.Comparing: e61c9e0...4179acc react-dom
react-art
react-test-renderer
react-native-renderer
react-events
react-reconciler
Generated by 🚫 dangerJS |
Could you provide some use case for hook form over component only form? |
@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
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>
</>
} |
@TrySound Yep, that's right :) |
This will decline the need for lots of custom hooks out there, very promising! |
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? |
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, It's also a bit confusing that there are two ways of using events: components and hooks.
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:
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 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? |
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 Doing the same thing with rich components is a pita. This primitive let us implement simple effects with really small syntax.
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. |
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. |
This doesn't seem too different to me, but it is much clearer about:
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? |
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.
In Sebastian's example the |
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. 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:
The important difference is that event components like 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 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>
<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 If you provide a way of handling 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:
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:
|
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):
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:
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. |
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. |
@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. |
@benjamingr You made some good points, I'll try and address them accordingly:
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! |
@trueadm thanks for the thoughtful reply, it is good to learn that you are taking automation and accessibility seriously and are testing them.
Would it be possible to integrate into them in production builds? 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.
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 :]
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.
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:
vs.
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). |
This doesn't sound accurate. React DevTools works in production, and ReactDOM connects to it regardless. |
This comment has been minimized.
This comment has been minimized.
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 When dealing with focus and modals, you'd use 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. |
@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 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 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 |
@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 There are a few use-cases in general with events:
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. |
There was a problem hiding this 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 = { |
There was a problem hiding this comment.
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.
I'll follow up on creating the dependency object lazily. Merging to unblock. |
Most fibers do not have events or context, so we save memory lazily initializing this container node. Follow-up from facebook#15927
Most fibers do not have events or context, so we save memory lazily initializing this container node. Follow-up from facebook#15927
Most fibers do not have events or context, so we save memory lazily initializing this container node. Follow-up from #15927
* [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
Most fibers do not have events or context, so we save memory lazily initializing this container node. Follow-up from facebook#15927
* [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
This PR adds a new primitive hook to React called
useEvent
. For now it's behind the React Flare flag and is labelledunstabled_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:
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:
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 hasallowEventHooks
set tofalse
.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.