React自定义Dropdown组件问题:打开新下拉框时旧的不关闭
实现Dropdown组件的互斥关闭功能(打开新下拉框时自动关闭已打开的其他下拉框)
我自行开发了一个Dropdown容器组件,目前可实现点击展开/收起、点击组件外部收起的功能,但存在问题:打开一个下拉框时,已打开的其他下拉框不会自动关闭,仅点击下拉框外部时才会全部关闭,现寻求解决方法。
现有逻辑代码(TypeScript)
const [showState, setShowState] = useState<boolean>(false); const dropdownRef = useRef<HTMLDivElement>(null); useEffect(() => { const $dropdown = dropdownRef.current; if (showState && $dropdown) { const handleClickOutside = (event: MouseEvent) => { if ($dropdown !== event.target && !$dropdown.contains(event.target as HTMLElement)) { setShowState(() => false); props.onToggle?.(false); } }; document.addEventListener('click', handleClickOutside); return () => { document.removeEventListener('click', handleClickOutside); }; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [showState, dropdownRef, setShowState, props.onToggle]); const handleTogglerClick = (event: React.MouseEvent<HTMLDivElement>) => { event.stopPropagation(); setShowState((prev: boolean) => { const newValue = !prev; props.onToggle?.(newValue); return newValue; }); }; const onSelectOption = (action: IDropdownAction<Actions>, index: number) => { setShowState(() => false); props.onToggle?.(false); props.onSelect?.(action, index); };
现有HTML结构
<div className={`${classes['container']} ${showState ? classes['container--active'] : ''} ${props.className}`.trim()}> <div className={`${classes['toggler']} ${props.disabled ? classes['toggler--disabled'] : ''}`.trim()} onClick={handleTogglerClick}> {props.children} {props.showArrowIcon ? ( <div className={classes['arrowContainer']}> <PIcon className={showState ? classes['arrowShow'] : classes['arrow']} name={showState ? 'dropDownArrowRight' : 'dropDownArrowDown'} ></PIcon> </div> ) : null} </div> <div className={`${classes['dropdownContainer']} ${classes[`dropdownContainer--${props.side}`]}`}> <MDropdown ref={props.dropdownRef} actions={props.actions} show={props.show} searchInput={props.searchInput} generalSearchPlaceholder={props.generalSearchPlaceholder} additionalElements={props.additionalElements} onSelect={props.onSelect} ></MDropdown> </div> </div>
现有CSS样式
.container { position: relative; background-color: white; display: flex; &--active { background-color: map-get($colors, dropdown-active); } } .toggler { cursor: pointer; position: relative; width: 100%; &--disabled { pointer-events: none; } .arrowContainer { position: absolute; top: 0; inset-inline-end: 0.5vw; height: 100%; display: flex; align-items: center; .arrow { width: 0.77vw; height: 1.1vw; } .arrowShow { width: 1.1vw; height: 0.77vw; } } } .dropdownContainer { position: absolute; z-index: map-get($layers, above_layout); &--right { left: 100%; top: 5%; } &--left { right: 100%; top: 5%; } &--leftTop { right: 100%; bottom: 0%; } &--up { left: 100%; bottom: -10%; } }
解决方法
方案1:使用自定义事件实现组件间通信
这种方式无需额外状态管理工具,通过浏览器自定义事件让所有Dropdown组件感知"关闭其他下拉框"的指令:
- 修改
handleTogglerClick方法,打开当前下拉框时派发自定义事件
const handleTogglerClick = (event: React.MouseEvent<HTMLDivElement>) => { event.stopPropagation(); setShowState((prev) => { const newValue = !prev; if (newValue) { // 通知其他Dropdown关闭 document.dispatchEvent(new CustomEvent('closeOtherDropdowns')); } props.onToggle?.(newValue); return newValue; }); };
- 添加监听自定义事件的
useEffect,收到指令后关闭当前下拉框(如果处于打开状态)
useEffect(() => { const handleCloseOthers = () => { if (showState) { setShowState(false); props.onToggle?.(false); } }; document.addEventListener('closeOtherDropdowns', handleCloseOthers); return () => { document.removeEventListener('closeOtherDropdowns', handleCloseOthers); }; }, [showState, setShowState, props.onToggle]);
方案2:使用React Context统一管理活跃状态
适合中大型项目,通过全局上下文维护当前唯一打开的Dropdown标识,实现精准控制:
- 创建Dropdown上下文
import React, { createContext, useContext, useState, ReactNode } from 'react'; type DropdownContextType = { activeDropdownId: string | null; setActiveDropdownId: (id: string | null) => void; }; const DropdownContext = createContext<DropdownContextType | undefined>(undefined); export const DropdownProvider = ({ children }: { children: ReactNode }) => { const [activeDropdownId, setActiveDropdownId] = useState<string | null>(null); return ( <DropdownContext.Provider value={{ activeDropdownId, setActiveDropdownId }}> {children} </DropdownContext.Provider> ); }; export const useDropdownContext = () => { const context = useContext(DropdownContext); if (!context) throw new Error('useDropdownContext必须在DropdownProvider内部使用'); return context; };
- 修改Dropdown组件,接入上下文
// 给组件props添加唯一id属性 type DropdownProps = { id: string; // 原有props... }; const DropdownContainer = (props: DropdownProps) => { const { id } = props; const { activeDropdownId, setActiveDropdownId } = useDropdownContext(); // 用上下文状态控制显示 const showState = activeDropdownId === id; const dropdownRef = useRef<HTMLDivElement>(null); useEffect(() => { const $dropdown = dropdownRef.current; if (showState && $dropdown) { const handleClickOutside = (event: MouseEvent) => { if ($dropdown !== event.target && !$dropdown.contains(event.target as HTMLElement)) { setActiveDropdownId(null); props.onToggle?.(false); } }; document.addEventListener('click', handleClickOutside); return () => document.removeEventListener('click', handleClickOutside); } }, [showState, dropdownRef, setActiveDropdownId, props.onToggle]); const handleTogglerClick = (event: React.MouseEvent<HTMLDivElement>) => { event.stopPropagation(); if (activeDropdownId === id) { // 关闭当前 setActiveDropdownId(null); props.onToggle?.(false); } else { // 打开当前,自动关闭其他 setActiveDropdownId(id); props.onToggle?.(true); } }; const onSelectOption = (action: IDropdownAction<Actions>, index: number) => { setActiveDropdownId(null); props.onToggle?.(false); props.onSelect?.(action, index); }; // 原有渲染代码... };
- 在根组件或父组件中包裹
DropdownProvider
import { DropdownProvider } from './DropdownContext'; const App = () => { return ( <DropdownProvider> <DropdownContainer id="dropdown-1" {...其他属性} /> <DropdownContainer id="dropdown-2" {...其他属性} /> </DropdownProvider> ); };
内容的提问来源于stack exchange,提问作者MayMay
相关产品推荐
相关产品推荐

