Material UI ClickAwayListener立即触发导致state重复切换问题
我针对该问题调研多日,最终决定在此提问。我在React项目中使用Material UI的ClickAwayListener组件,业务需求为:通过按钮切换this.state.showGridPopup的布尔值,单次点击按钮展示网格弹窗,再次点击则隐藏弹窗。按钮绑定了handleShowGrid处理函数实现该逻辑,代码如下:
handleShowGrid = (event) => { const { widgetButtonEl } = this.state; const element = widgetButtonEl === null ? event.currentTarget : null; console.log('In handleShowGrid!!!!!'); this.setState({ showWidget: !this.state.showWidget, widgetButtonEl: element }); }
上述逻辑本身运行正常,点击绑定了处理函数的按钮可正常切换弹窗显隐,对应按钮代码如下:
<ButtonBase onClick={this.handleShowGrid}>Open Swap</ButtonBase>
问题出现在引入Material UI的ClickAwayListener之后,引入该组件的目的是实现用户点击网格外部区域时关闭网格,相关代码如下:
<ClickAwayListener onClickAway={this.handleCloseWidget}> <SurveyGrid /> </ClickAwayListener>
对应的关闭处理函数代码如下:
handleCloseGrid = (e) => { console.log('In handleCloseWidget!!!!!'); this.setState({ showWidget: !this.state.showWidget, widgetButtonEl: null }); }
目前点击网格外部区域可正常关闭网格,但点击用于切换网格显隐的按钮(控制this.state.showWidget)时,会先触发handleCloseGrid,随后才触发handleShowGrid,导致状态被切换两次。我初步判断该问题与事件传播、事件冒泡有关,已尝试e.stopPropagation()等方案但均未生效,需要找到避免点击切换按钮时误触发handleCloseGrid处理函数的方案。
e.stopPropagation()不生效的核心原因是:Material UI 的 ClickAwayListener 默认绑定在 document 层的事件捕获阶段,触发时机早于按钮绑定在冒泡阶段的onClick回调,在按钮回调里阻止冒泡根本拦截不到已经执行完的捕获阶段监听。
同时切换按钮在ClickAwayListener的包裹范围外,点击按钮时会被组件判定为「点击了监听区域外部」,先执行关闭逻辑把状态翻为false,之后按钮自身的点击回调再执行一次把状态翻回true,两次状态翻转后最终表现和预期完全不符。
优先选第一种实现,稳定性最高:
- 方案1:关闭回调中过滤触发按钮的点击
给切换按钮绑定ref,在关闭逻辑里判断点击目标如果落在按钮区域内,直接跳过关闭逻辑。另外注意关闭弹窗时不要用状态取反,直接将显隐状态设为false,避免并发更新下的状态异常。// 组件构造函数里先初始化ref:this.gridTriggerRef = React.createRef() // 按钮绑定ref <ButtonBase onClick={this.handleShowGrid} ref={this.gridTriggerRef} > Open Swap </ButtonBase> // 修正关闭逻辑 handleCloseGrid = (e) => { // 点击的是触发按钮,不执行关闭 if (this.gridTriggerRef.current?.contains(e.target)) return this.setState({ showWidget: false, widgetButtonEl: null }) } - 方案2:调整ClickAwayListener的事件监听阶段
通过组件props把监听事件从默认的捕获阶段onMouseDown改成冒泡阶段触发的事件,此时在按钮回调里阻止冒泡即可拦截关闭逻辑。
该方案存在边缘case:如果用户在弹窗内部按下鼠标,拖动到按钮区域再松开,会出现判定失效,非必要不选。<ClickAwayListener onClickAway={this.handleCloseGrid} mouseEvent="onMouseUp" > <SurveyGrid /> </ClickAwayListener> // 按钮回调加阻止冒泡 handleShowGrid = (event) => { event.stopPropagation() const { widgetButtonEl } = this.state const element = widgetButtonEl === null ? event.currentTarget : null this.setState({ showWidget: !this.state.showWidget, widgetButtonEl: element }) }
内容的提问来源于stack exchange,提问作者Kimbo

