Feature Request: Event Modifiers support

This issue has been tracked since 2022-09-08.

I'm moving from Vue to React recently, and one of the few practical features I miss is native Event Modifiers support.

Vue supports several types of native events that make development very convenient. The ones I miss the most are:

  • .stop: same as e.stopPropagation().
  • .prevent: same as e.preventDefault();

Pros:

  • Allows the function defined in the handle to perform its main function without worrying about the event flow;
  • Facilitates the reuse of callbacks that control the flow differently, but achieve the same result;
function handleClick(e) {
    console.log('ok');
}

function handleClickWithStop(e) {
    e.stopPropagation();
    handleClick(e);
}
<div onClick={handleClickWithStop}>must stop</div>
<div onClick={handleClick}>must continue</div>

Becomes:

function handleClick(e) {
    console.log('ok');
}
<div onClick.stop={handleClick}>must stop</div>
<div onClick={handleClick}>must continue</div>

Cons:

The only thing I can think of being "against" is that it might increase the cost of converting from JSX to native JS. Although I think this only happens during the build process. Right?

During the conversion process, events will need to identify the .indexOf('.') and then apply the necessary wrappers:

React.createElement(
    'div',
    { onClick: function (e) {
          e.stopPropagation();
          handleClick(e);
      } }
)
markerikson wrote this answer on 2022-09-10

This is really a request for a change to the JSX syntax definition and compilers like Babel, TypeScript, and ESBuild, not React itself.

Given that, this is highly unlikely to ever happen, for a few reasons:

  • JSX is intended to be a fairly straightforward transformation into plain JS function calls, and this adds additional complexity
  • The JSX spec has been frozen for years, and even relatively simple proposed changes haven't happened
  • This would require coordination across a massive ecosystem of tools
rentalhost wrote this answer on 2022-09-10

@markerikson that makes complete sense. I really thought that React performed some kind of transformation on the processed JSX and had greater control over it (maybe it is possible from the React side, but in fact, ideally it would be part of the JSX if that was the case).

markerikson wrote this answer on 2022-09-10

Yeah, strictly speaking the JSX transform isn't part of "React" the software library at all. JSX was invented for use with React, but many tools now implement that syntax (even Vue!), and the output is just the function calls to React.createElement() or the equivalent for other libraries.

If you want to get a sense of some other proposed changes to the JSX spec (none of which have become reality), see the discussions in this thread:

gaearon wrote this answer on 2022-09-11

I’m not sure the biggest problem here is with JSX being frozen. I think my main objection would be that it “feels” like specifying information in a wrong place.

In this proposal whether to stop propagation or not seems to be specified as part of the event key. So it’s as if we’re saying that onClick.stop is a separate event from onClick. What happens if you specify both? Do both run or does one override the other? Does it error?

What if you do <div onClick {…props}> and props contains an object with the key onClick.stop because the parent passed it to your custom component? Do they clash? Or does this prop only work on built-ins? If it only works on built-ins, how are you supposed to refactor the code when you wrap a component? How do you destructure the prop in your component given . is not a valid part of the identifier?

What if you want to render a third-party component that only takes onClick but you want it to stop propagation. Do you do it manually? Does it have to support both versions explicitly? Does the .stop syntax actually rewrite the function that you pass to it? What if you define this function separately like onClick.stop={handleClick}? Does this attach some metadata to the function itself? Does this wrap it into a special object and make it opaque?

Is this syntax only for event handlers? What about other functions being passed down?

I think there could be something to explore with event handler metadata. Like passive events (which we don’t support at all), or bubble/capture (which we support via Capture suffix) are two real dimensions. Declarative prevent/stop could maybe be a thing too. But I’d need to understand the proposed design a lot more. So far it’s not clear to me how this can work, and I’m not familiar with how it works with Vue. In particular we care a lot about code using custom components looking good and working well, so it needs to be a first-class consideration. We can’t have built-ins with more “conveniences” than custom components.

rentalhost wrote this answer on 2022-09-12

@gaearon your question is very interesting. I'll answer based on what vue does in this case:

When an element has only one @click="handleClick", for example, we get something like:

_createElementVNode("div", {
    onClick: _cache[0] || (_cache[0] = (...args)=>(_ctx.handleClick && _ctx.handleClick(...args)))
})

However, if we have a definition @click and @click.stop at the same time, we will have:

_createElementVNode("div", {
    onClick: [
        _cache[0] || (_cache[0] = (...args)=>(_ctx.handleClick && _ctx.handleClick(...args))), 
        _cache[1] || (_cache[1] = _withModifiers((...args)=>(_ctx.handleClick && _ctx.handleClick(...args)), ["stop"]))
    ]
})

Which indicates that vue accepts this condition well, transforming .onClick into an array of events, instead of a direct event (like the first case).

Not only that, but @click (and events in general) allows the use of an array containing multiple definitions for methods, so the same event can perform different functions without a wrapper: @click="[ handleClick($event), handleClickTwo($event) ]"

_createElementVNode("div", {
    onClick: _cache[0] || (_cache[0] = $event=>([$setup.handleClick($event), $setup.handleClickTwo($event)]))
})

As for the example of ...props, the code @click="handleClick" v-bind:onClick="handleClickTwo" is converted to:

_createElementVNode("div", {
    onClick: [$setup.handleClick, $setup.handleClickTwo]
}, "teste")

As for third-party code, you can even define both @click and @click.stop with independent supports, but at this point it would be overkill. If the code user needs a stop beyond what is developed by the third party code, then he will have to do it directly by the JS.

In vue, events are declared explicitly using @. Which means that @something is necessarily an event, and stop and prevent support are applicable exclusively in these cases. In addition, there is other support for specific events such as @keydown.enter, @keydown.space etc. I don't think anything at this level can be done in React.

More Details About Repo
Owner Name facebook
Repo Name react
Full Name facebook/react
Language JavaScript
Created Date 2013-05-24
Updated Date 2022-10-03
Star Count 195549
Watcher Count 6650
Fork Count 40505
Issue Count 1119

YOU MAY BE INTERESTED

Issue Title Created Date Updated Date