react icon indicating copy to clipboard operation
react copied to clipboard

Feature Request: Event Modifiers support

Open rentalhost opened this issue 3 years ago • 6 comments

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);
      } }
)

rentalhost avatar Sep 08 '22 16:09 rentalhost

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

markerikson avatar Sep 10 '22 21:09 markerikson

@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).

rentalhost avatar Sep 10 '22 21:09 rentalhost

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:

  • https://github.com/facebook/jsx/issues/65

markerikson avatar Sep 10 '22 21:09 markerikson

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.

gaearon avatar Sep 11 '22 17:09 gaearon

@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.

rentalhost avatar Sep 11 '22 19:09 rentalhost

This is a really handy feature in Vue's syntax, one that I constantly feel React lacks.

Beyond just being syntactic sugar, it streamlines the focus of event handlers towards solely managing data/UI logic.

farzadso avatar Feb 29 '24 10:02 farzadso

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

github-actions[bot] avatar May 29 '24 11:05 github-actions[bot]

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!

github-actions[bot] avatar Jun 05 '24 11:06 github-actions[bot]