Consider the following typical React document structure:
Component.jsx
<OuterClickableArea>
<InnerClickableArea>
content
</InnerClickableArea>
</OuterClickableArea>
Where these components are composed as follows:
OuterClickableArea.js
export default class OuterClickableArea extends React.Component {
constructor(props) {
super(props)
this.state = {
clicking: false
}
this.onMouseDown = this.onMouseDown.bind(this)
this.onMouseUp = this.onMouseUp.bind(this)
}
onMouseDown() {
if (!this.state.clicking) {
this.setState({ clicking: true })
}
}
onMouseUp() {
if (this.state.clicking) {
this.setState({ clicking: false })
this.props.onClick && this.props.onClick.apply(this, arguments)
console.log('outer area click')
}
}
render() {
return (
<div
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
>
{ this.props.children }
</div>
)
}
}
With, for the sake of this argument, InnerClickableArea.js having pretty much the same code except for the class name and the console log statement.
Now if you were to run this app and a user would click on the inner clickable area, the following would happen (as expected):
This displays typical event bubbling/propagation -- no surprises so far.
It will output:
inner area click
outer area click
Now, what if we are creating an app where only a single interaction can happen at a time? For example, imagine an editor where elements could be selected with mouse clicks. When pressed inside the inner area, we would only want to select the inner element.
The simple and obvious solution would be to add a stopPropagation inside the InnerArea component:
InnerClickableArea.js
onMouseDown(e) {
if (!this.state.clicking) {
e.stopPropagation()
this.setState({ clicking: true })
}
}
This works as expected. It will output:
inner area click
The problem?
InnerClickableArea
hereby implicitly chose for OuterClickableArea
(and all other parents) not to be able to receive an event. Even though InnerClickableArea
is not supposed to know about the existence of OuterClickableArea
. Just like OuterClickableArea
does not know about InnerClickableArea
(following separation of concerns and reusability concepts). We created an implicit dependency between the two components where OuterClickableArea
"knows" that it won't mistakenly have its listeners fired because it "remembers" that InnerClickableArea would stop any event propagation. This seems wrong.
I'm trying not to use stopPropagation to make the app more scalable, because it would be easier to add new features that rely on events without having to remember which components use stopPropagation
at which time.
Instead, I would like to make the above logic more declarative, e.g. by defining declaratively inside Component.jsx
whether each of the areas is currently clickable or not. I would make it app state aware, passing down whether the areas are clickable or not, and rename it to Container.jsx
. Something like this:
Container.jsx
<OuterClickableArea
clickable={!this.state.interactionBusy}
onClicking={this.props.setInteractionBusy.bind(this, true)} />
<InnerClickableArea
clickable={!this.state.interactionBusy}
onClicking={this.props.setInteractionBusy.bind(this, true)} />
content
</InnerClickableArea>
</OuterClickableArea>
Where this.props.setInteractionBusy
is a redux action that would cause the app state to be updated. Also, this container would have the app state mapped to its props (not shown above), so this.state.ineractionBusy
will be defined.
OuterClickableArea.js (almost same for InnerClickableArea.js)
export default class OuterClickableArea extends React.Component {
constructor(props) {
super(props)
this.state = {
clicking: false
}
this.onMouseDown = this.onMouseDown.bind(this)
this.onMouseUp = this.onMouseUp.bind(this)
}
onMouseDown() {
if (this.props.clickable && !this.state.clicking) {
this.setState({ clicking: true })
this.props.onClicking && this.props.onClicking.apply(this, arguments)
}
}
onMouseUp() {
if (this.state.clicking) {
this.setState({ clicking: false })
this.props.onClick && this.props.onClick.apply(this, arguments)
console.log('outer area click')
}
}
render() {
return (
<div
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
>
{ this.props.children }
</div>
)
}
}
The problem is that the Javascript event loop seems to run these operations in the following order:
OuterClickableArea
and InnerClickableArea
are created with prop clickable
equal to true
(because app state interactionBusy defaults to false)interactionBusy
is set to true
clickable
prop is still true
because even though the app state interactionBusy
is true
, react did not yet cause a re-render; the render operation is put at the end of the Javascript event loop)Component.jsx
and passes in clickable
as false
to both components, but by now this is too lateThis will output:
inner area click
outer area click
Whereas the desired output is:
inner area click
App state, therefore, does not help in trying to make these components more independent and reusable. The reason seems to be that even though the state is updated, the container only re-renders at the end of the event loop, and the mouse event propagation triggers are already queued in the event loop.
My question, therefore, is: is there an alternative to using app state to be able to work with mouse event propagation in a declarative manner, or is there a better way to implement/execute my above setup with app (redux) state?
Easiest alternative to this is to add a flag before firing stopPropogation and the flag in this case is an argument.
const onMouseDown = (stopPropagation) => {
stopPropagation && event.stopPropagation();
}
Now even the application state or prop can even decide to trigger the stoppropagation
<div onMouseDown={this.onMouseDown(this.state.stopPropagation)} onMouseUp={this.onMouseUp(this.props.stopPropagation)} >
<InnerComponent stopPropagation = {true}>
</div>
As @Rikin mentioned and as you alluded to in your answer, event.stopPropagation would solve the your issue in its current form.
I'm trying not to use stopPropagation to make the app more scalable, because it would be easier to add new features that rely on events without having to remember which components use stopPropagation at which time.
Is this not a premature concern in terms of the current scope of your component architecture? If you wish for innerComponent
to be nested within outerClickableArea
the only way to achieve that is to stopPropagation or to somehow determine which events to ignore. Personally, I would stop propagation for all events, and for those click events for which you selectively want to share state between components, capture the events and dispatch an action (if using redux) to the global state. That way you don't have to worry about selectively ignoring/capturing bubbled events and can update a global source of truth for those events that imply shared state
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With