首页 > 解决方案 > React.js Dropdown 组件不会在按钮单击时隐藏

问题描述

我在制作可点击Dropdown组件时遇到了问题。我的任务是在单击按钮时显示菜单,并在用户单击文档中的任何位置或单击同一个按钮时隐藏菜单,所有组件也应该是功能组件。

我正在使用名为的 3rd 方包classnames,它有助于有条件地加入CSS类,还使用 ​​aReact ContextAPI将道具传递给Dropdown子组件。

Dropdown组件依赖于 2 个子组件。

DropdownToggle - (呈现一个可点击的按钮)

DropdownMenu - (呈现带有菜单项的 div)

问题:

每当我打开菜单并单击文档菜单中的任意位置时,它都可以正常工作,但是当我打开菜单并想用按钮隐藏时,它就不起作用了。我认为问题useEffect出在组件的钩子内部Dropdown

代码沙盒

演示:

落下

这是App渲染所有组件的主要组件。

应用程序.js

import React, { Component } from "react";
import Dropdown from "./Dropdown";
import DropdownToggle from "./DropdownToggle";
import DropdownMenu from "./DropdownMenu";
import "./dropdown.css";

// App component
class App extends Component {
  state = {
    isOpen: false
  };

  toggle = () => {
    alert("Button is clicked");
    this.setState({
      isOpen: !this.state.isOpen
    });
  };

  render() {
    return (
      <div className="app">
        <Dropdown isOpen={this.state.isOpen} toggle={this.toggle}>
          <DropdownToggle>Dropdown</DropdownToggle>
          <DropdownMenu>
            <div>Item 1</div>
            <div>Item 2</div>
          </DropdownMenu>
        </Dropdown>
      </div>
    );
  }
}

export default App;

主要src代码:

DropdownContext.js

import {createContext} from 'react';
// It is used on child components.
export const DropdownContext = createContext({});
// Wrap Dropdown with this Provider.
export const DropdownProvider = DropdownContext.Provider;

Dropdown.js

import React, { useEffect } from "react";
import classNames from "classnames";
import { DropdownProvider } from "./DropdownContext";


/**
 * Returns a new object with the key/value pairs from `obj` that are not in the array `omitKeys`.
 * @param obj
 * @param omitKeys
 */
const omit = (obj, omitKeys) => {
  const result = {};
  // Get object properties as an array
  const propsArray = Object.keys(obj);
  propsArray.forEach(key => {
    // Searches the array for the specified item, if the item is not found it returns -1 then
    // construct a new object and return it.
    if (omitKeys.indexOf(key) === -1) {
      result[key] = obj[key];
    }
  });
  return result;
};

// Dropdown component
const Dropdown = props => {
  // Populate context value based on the props
  const getContextValue = () => {
    return {
      toggle: props.toggle,
      isOpen: props.isOpen
    };
  };

  // toggle function
  const toggle = e => {
    // Execute toggle function which is came from the parent component
    return props.toggle(e);
  };

  // handle click for the document object
  const handleDocumentClick = e => {
    // Execute toggle function of the parent
    toggle(e);
  };

  // Remove event listeners
  const removeEvents = () => {
    ["click", "touchstart"].forEach(event =>
      document.removeEventListener(event, handleDocumentClick, true)
    );
  };

  // Add event listeners
  const addEvents = () => {
    ["click", "touchstart"].forEach(event =>
      document.addEventListener(event, handleDocumentClick, true)
    );
  };

  useEffect(() => {
    const handleProps = () => {
      if (props.isOpen) {
        addEvents();
      } else {
        removeEvents();
      }
    };
    // mount
    handleProps();
    // unmount
    return () => {
      removeEvents();
    };
  }, [props.isOpen]);

  // Condense all other attributes except toggle `prop`.
  const { className, isOpen, ...attrs } = omit(props, ["toggle"]);
  // Conditionally join all classes
  const classes = classNames(className, "dropdown", { show: isOpen });

  return (
    <DropdownProvider value={getContextValue()}>
      <div className={classes} {...attrs} />
    </DropdownProvider>
  );
};

export default Dropdown;

Dropdown组件有一个父组件,即Provider每当Provider值发生变化时,子组件将访问这些值。其次,在 DOM 上,它将呈现一个divDropdown标记结构组成的结构。

DropdownToggle.js

import React, {useContext} from 'react';
import classNames from 'classnames';
import {DropdownContext} from './DropdownContext';

// DropdownToggle component
const DropdownToggle = (props) => {

    const {toggle} = useContext(DropdownContext);

    const onClick = (e) => {
        // If props onClick is not undefined
        if (props.onClick) {
            // execute the function
            props.onClick(e);
        }
        toggle(e);
    };

    const {className, ...attrs} = props;

    const classes = classNames(className);


    return (
        // All children would be render inside this. e.g. `svg` & `text`
        <button type="button" className={classes} onClick={onClick} {...attrs}/>
    );
};

export default DropdownToggle;

下拉菜单.js

import React, { useContext } from "react";
import classNames from "classnames";
import { DropdownContext } from "./DropdownContext";

// DropdownMenu component
const DropdownMenu = props => {
  const { isOpen } = useContext(DropdownContext);

  const { className, ...attrs } = props;
  // add show class if isOpen is true
  const classes = classNames(className, "dropdown-menu", { show: isOpen });

  return (
    // All children would be render inside this `div`
    <div className={classes} {...attrs} />
  );
};

export default DropdownMenu;

标签: reactjs

解决方案


Jayce444答案是正确的。当您单击按钮时,它会触发一次,然后事件会冒泡到文档并再次触发。

我只想为您添加另一个替代解决方案。您可以使用useRef钩子创建Dropdown节点的引用并检查当前事件目标是否为button元素。将此代码添加到您的Dropdown.js文件中。

import React, { useRef } from "react";

const Dropdown = props => {
  const containerRef = useRef(null);

  // get reference of the current div
  const getReferenceDomNode = () => {
    return containerRef.current;
  };

  // handle click for the document object
  const handleDocumentClick = e => {
    const container = getReferenceDomNode();
    if (container.contains(e.target) && container !== e.target) {
      return;
    }
    toggle(e);
  };

  //....

  return (
    <DropdownProvider value={getContextValue()}>
      <div className={classes} {...attrs} ref={containerRef} />
    </DropdownProvider>
  );
};
export default Dropdown;

推荐阅读