admin管理员组文章数量:1325236
Consider the following typical React document structure:
Component.jsx
<OuterClickableArea>
<InnerClickableArea>
content
</InnerClickableArea>
</OuterClickableArea>
Where these ponents are posed 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):
- outer area registers mouse event handlers
- inner area registers mouse event handlers
- user presses mouse down in the inner area
- inner area's mouseDown listener triggers
- outer area's mouseDown listener triggers
- user releases mouse up
- inner area's mouseUp listener triggers
- console logs "inner area click"
- outer area's mouseUp listener triggers
- console logs "outer area click"
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 ponent:
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 ponents 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 ponents 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:
- both
OuterClickableArea
andInnerClickableArea
are created with propclickable
equal totrue
(because app state interactionBusy defaults to false) - outer area registers mouse event handlers
- inner area registers mouse event handlers
- user presses mouse down in the inner area
- inner area's mouseDown listener triggers
- inner area's onClicking is fired
- container runs 'setInteractionBusy' action
- redux app state
interactionBusy
is set totrue
- outer area's mouseDown listener triggers (it's
clickable
prop is stilltrue
because even though the app stateinteractionBusy
istrue
, react did not yet cause a re-render; the render operation is put at the end of the Javascript event loop) - user releases mouse up
- inner area's mouseUp listener triggers
- console logs "inner area click"
- outer area's mouseUp listener triggers
- console logs "outer area click"
- react re-renders
Component.jsx
and passes inclickable
asfalse
to both ponents, but by now this is too late
This 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 ponents 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?
Consider the following typical React document structure:
Component.jsx
<OuterClickableArea>
<InnerClickableArea>
content
</InnerClickableArea>
</OuterClickableArea>
Where these ponents are posed 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):
- outer area registers mouse event handlers
- inner area registers mouse event handlers
- user presses mouse down in the inner area
- inner area's mouseDown listener triggers
- outer area's mouseDown listener triggers
- user releases mouse up
- inner area's mouseUp listener triggers
- console logs "inner area click"
- outer area's mouseUp listener triggers
- console logs "outer area click"
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 ponent:
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 ponents 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 ponents 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:
- both
OuterClickableArea
andInnerClickableArea
are created with propclickable
equal totrue
(because app state interactionBusy defaults to false) - outer area registers mouse event handlers
- inner area registers mouse event handlers
- user presses mouse down in the inner area
- inner area's mouseDown listener triggers
- inner area's onClicking is fired
- container runs 'setInteractionBusy' action
- redux app state
interactionBusy
is set totrue
- outer area's mouseDown listener triggers (it's
clickable
prop is stilltrue
because even though the app stateinteractionBusy
istrue
, react did not yet cause a re-render; the render operation is put at the end of the Javascript event loop) - user releases mouse up
- inner area's mouseUp listener triggers
- console logs "inner area click"
- outer area's mouseUp listener triggers
- console logs "outer area click"
- react re-renders
Component.jsx
and passes inclickable
asfalse
to both ponents, but by now this is too late
This 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 ponents 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?
Share Improve this question edited May 22, 2018 at 18:18 Eby Jacob 1,4581 gold badge12 silver badges29 bronze badges asked May 11, 2018 at 18:43 TomTom 8,13735 gold badges140 silver badges237 bronze badges 3-
1
Event gets captured and then bubbled up. Solution is probably to stopPropagation at some point. You can probably add it in the event bind call of site appended after
;
likeonClicking={this.props.setInteractionBusy.bind(this, true); $event.stopPropagation();}
– Rikin Commented May 11, 2018 at 19:22 - @Rikin I don't mind that it's bubbling up, but while bubbling, I would like for other ponents to be able to review app state and determine independently whether they want to act or not, without implicitly depending on other ponents. – Tom Commented May 11, 2018 at 19:45
-
Will consider your suggestion to move the
stopPropagation
call one level higher though. In essence that achieves what I want, though it is a little hard to follow (less descriptive than setting whether something is clickable or not). – Tom Commented May 11, 2018 at 19:49
5 Answers
Reset to default 3As @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 ponents use stopPropagation at which time.
Is this not a premature concern in terms of the current scope of your ponent 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 ponents, 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
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>
Instead of handling the click event and clickable
state in the ponent, use a LOCK
state to Redux state and when user click, the ponents just emit a CLICK
action, hold the lock and do the logic. Like this:
- Both OuterClickableArea and InnerClickableArea are created
- Redux state with the field
CLICK_LOCK
default tofalse
. - Outer area registers mouse event handlers
- Inner area registers mouse event handlers
- User click in inner area
- Inner area's
mouseDown
listener triggers - Inner area emits action
INNER_MOUSEDOWN
- Outer area's
mouseDown
listener triggers - Outer area emits action
OUTER_MOUSEDOWN
INNER_MOUSEDOWN
action set theCLICK_LOCK
state totrue
and do logic (loginner area click
).- Because
CLICK_LOCK
istrue
,OUTER_MOUSEDOWN
do nothing. - Inner area's
mouseUp
listener triggers - Inner area emits action
INNER_MOUSEUP
- Outer area's
mouseUp
listener triggers - Outer area emits action
OUTER_MOUSEUP
INNER_MOUSEUP
action set theCLICK_LOCK
state tofalse
.OUTER_MOUSEUP
action set theCLICK_LOCK
state tofalse
.- Nothing have to be re-render unless the click logic change some state.
Note:
- This approach work by having a lock in a region, at one time, only one ponent can
click
. Each ponent do not have to know about the others, but need a global state (Redux) to orchestrate. - If the rate of clicking is high, this approach performance might not be good.
- Currently Redux actions is guaranteed to be emitted synchronously. Not sure about the future.
I'm suspecting that you are about to create something like overlay modal, where there will be ponent that should be closed when the outer ponent or the container ponent was clicked, if that's the case, you could use react-overlays instead of recreating the whole thing, you could also learn on how they handle this kind of implementation if you have different purpose.
Have you though of passing the information if the event was already processed in the event itself?
const handleClick = e => {
if (!e.nativeEvent.wasProcessed) {
// do something
e.nativeEvent.wasProcessed = true;
}
};
This way the outer ponent can check that the event was already processed by one of it's childrens while the event will still bubble up.
I've put up a sample that nests two Clickable ponents. The mousedown event gets processed only by the innermost Clickable ponent user clicked on: https://codesandbox.io/s/2wmxxzormy
Disclaimer: I could not find any information on this exactly being a bad practice, but in general modifying an object you do not own can lead to e.g. naming collisions in the future.
本文标签: javascriptHow to achieve reuseable components with React and mouse event propagationStack Overflow
版权声明:本文标题:javascript - How to achieve re-useable components with React and mouse event propagation? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1742162830a2425273.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论