There have been some attempts to answer this question:
here, here and here. However none of the answers are giving a solid response. I'm not referring to the event
phases capture
, bubble
and target
and how stopPropagation()
affects the overall event. I'm looking for a case where adding stopPropagation()
to a DOM node will benefit the overall code?
This really shouldn't be an answer, but there is only so much you can write in a single comment
I don't think you're doing your question justice by having the words "good practice" in its title. This sort of implies that in most cases, stopPropagation
is bad practice. This is similar to saying that eval is evil. It completely brushes off any legitimate use cases of it with misplaced dogmatism.
I never found myself in a situation where using stopPropagation
didn't feel like a workaround to avoid fixing the real issue.
In an ideal world, applications are built out of smaller components that do very little on their own but are highly reusable and combinable. For this to work, the recipe is simple yet very difficult to execute: each component must know nothing about the outside world.
Therefore if a component needs to use stopPropagation()
, it can only be because it knows that something further up the chain will break or that it will put your application into an undesirable state.
In this case you should be asking yourself whether that is not a symptom of a design issue. Perhaps you need a component that orchestrates and manages the events of its children?
You should also consider the fact that preventing the propagation of events can cause other components to misbehave. The classic example is a drop-down that closes when you click outside of it. If that click is stopped, your drop-down may never close.
Think of events as sources of data. You don't want to lose data. Au contraire! Let it go, let it free ;)
While I don't see using stopPropagation
as bad or evil practice, I just don't think it is ever needed.
Example: how to avoid using stopPropagation
In this example we're building a very simple game: if you click on a red area you lose, on a green area you win. The game is over once a click is made.
const onClick = (selector, handler) => {
document.querySelector(selector).addEventListener('click', handler);
};
onClick('#game', () => console.log('game over'));
onClick('#red', () => console.log('you lost'));
onClick('#green', () => console.log('you won'));
#red, #green { width: 50px; height: 50px; display: inline-block; }
#red { background-color: orangered; }
#green { background-color: yellowgreen; }
<div id="game">
<div id="red"></div>
<div id="green"></div>
</div>
Now let's imagine that there are different levels in which red and green blocks are arranged randomly. In level #42, the red block contains the green one.
const onClick = (selector, handler) => {
document.querySelector(selector).addEventListener('click', handler);
};
onClick('#game', () => console.log('game over'));
onClick('#red', () => console.log('you lost'));
onClick('#green', () => console.log('you won'));
#red, #green { max-width: 100px; padding: 10px; }
#red { background-color: orangered; }
#green { background-color: yellowgreen; }
<div id="game">
<div id="red">
<div id="green"></div>
</div>
</div>
As you can see when you click on the green area, you both win and lose at the same time! And if you were to put a stopPropagation()
call in the green handler, there will be no way to win this game since the click won't bubble up to the game handler to signal the end of the game!
Solution 1: identify the origin of the click
const filter = handler => ev =>
ev.target === ev.currentTarget ? handler(ev) : null;
const onClick = (selector, handler) => {
document.querySelector(selector).addEventListener('click', handler);
};
onClick('#game', () => console.log('game over'));
onClick('#red', filter(() => console.log('you lost')));
onClick('#green', () => console.log('you won'));
#red, #green { max-width: 100px; padding: 10px; }
#red { background-color: orangered; }
#green { background-color: yellowgreen; }
<div id="game">
<div id="red">
<div id="green"></div>
</div>
</div>
The key function is filter
. It will make sure that handler
will only execute if the click actually originated from the node itself and not from one of its children.
The currentTarget read-only property of the Event interface identifies the current target for the event, as the event traverses the DOM. It always refers to the element to which the event handler has been attached, as opposed to Event.target, which identifies the element on which the event occurred.
https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget
Solution 2: use event delegation
You don't actually need three events handlers. Just set up one on the #game
node.
const onClick = (selector, handler) => {
document.querySelector(selector).addEventListener('click', handler);
};
onClick('#game', (ev) => {
if (ev.target.id === 'red') {
console.log('you lost');
} else if (ev.target.id === 'green') {
console.log('you won');
}
console.log('game over');
});
#red, #green { max-width: 100px; padding: 10px; }
#red { background-color: orangered; }
#green { background-color: yellowgreen; }
<div id="game">
<div id="red">
<div id="green"></div>
</div>
</div>
stopPropagation()
. I hope that you (and @Kleo) find them useful. –
Beni © 2022 - 2024 — McMap. All rights reserved.
stopPropagation()
when you don't want the event to propagate further. Yes, I just told you "you need it when you need it" but I don't see a way to explain this that's not circular if you already know what propagation is. – ClantonstopPropagation
stops. That's why I don't understand what you don't understand aboutstopPropagation
. "How is this even remotely close to what I'm asking?" Let's be even more clear - let's say you want only the button click handler to engage, not the others. But you still want the other click handers that do different stuff. You also can't change the HTML. Your only option isstopPropagation
. You have to use it, so you use it. – Clantonfor
loop" and then "but why would I usefor
loop" when I give you an example. – Clanton