You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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组件感知"关闭其他下拉框"的指令:

  1. 修改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;
  });
};
  1. 添加监听自定义事件的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标识,实现精准控制:

  1. 创建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;
};
  1. 修改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);
  };

  // 原有渲染代码...
};
  1. 在根组件或父组件中包裹DropdownProvider
import { DropdownProvider } from './DropdownContext';

const App = () => {
  return (
    <DropdownProvider>
      <DropdownContainer id="dropdown-1" {...其他属性} />
      <DropdownContainer id="dropdown-2" {...其他属性} />
    </DropdownProvider>
  );
};

内容的提问来源于stack exchange,提问作者MayMay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 09:37:03