I am new to React and want to design a navigation bar with dropdown menu. When I click outside of the dropdown menu, it can be closed. And It works fine.
But the problem is when I click "settings", the dropdown menu is closed as well, with a warning:
Warning: findDOMNode is deprecated in StrictMode. findDOMNode was passed an instance of Transition which is inside StrictMode. Instead, add a ref directly to the element you want to reference.
I think the problem comes from the navItemRef
or the open
State since when I click "settings", the navItemRef
will not contain chilren
here {open && children}
.
Here is the complete code:
import { PropsWithChildren, useEffect, useRef, useState } from "react";
import { CSSTransition } from "react-transition-group";
import styles from "./styles.module.css";
const NavBar: React.FC<PropsWithChildren> = ({ children }) => {
return (
<nav className={styles.navbar}>
<ul className={styles["navbar-nav"]}>{children}</ul>
</nav>
);
};
interface INavItemProps extends PropsWithChildren {
icon: JSX.Element | string;
}
const NavItem: React.FC<INavItemProps> = ({ icon, children }) => {
const [open, setOpen] = useState<boolean>(false);
const navItemRef = useRef<HTMLLIElement>(null);
// !!! The wrong place
useEffect(() => {
const handleOutsideClick = (evt: MouseEvent) => {
if (!navItemRef.current?.contains(evt.target as Node)) {
setOpen(false);
}
};
document.addEventListener("click", handleOutsideClick);
return () => {
document.removeEventListener("click", handleOutsideClick);
};
}, []);
return (
<li className={styles["nav-item"]} ref={navItemRef}>
<a
href="#"
className={styles["icon-button"]}
onClick={() => setOpen(!open)}
>
{icon}
</a>
{open && children}
</li>
);
};
type DropDownItemType = "main" | "center" | "settings";
interface IDropDownItemProps extends PropsWithChildren {
toMenu?: DropDownItemType;
leftIcon?: JSX.Element | string;
rightIcon?: JSX.Element | string;
}
const DropdownMenu: React.FC = () => {
const [activeMenu, setActiveMenu] = useState<DropDownItemType>("main");
const [menuHeight, setMenuHeight] = useState<number>(0);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const firstChild = dropdownRef.current?.firstChild as HTMLDivElement;
setMenuHeight(firstChild?.offsetHeight);
}, []);
const calcHeight = (el: HTMLElement) => {
const height = el.offsetHeight;
setMenuHeight(height);
};
const DropDownItem: React.FC<IDropDownItemProps> = ({
toMenu,
leftIcon,
rightIcon,
children,
}) => {
return (
<a
href="#"
className={styles["menu-item"]}
onClick={() => toMenu && setActiveMenu(toMenu)} // toMenu might be undefined
>
<span className={styles["icon-button"]}>{leftIcon}</span>
{children}
<span className={styles["icon-right"]}>{rightIcon}</span>
</a>
);
};
return (
<div
className={styles.dropdown}
style={{ height: menuHeight }}
ref={dropdownRef}
>
<CSSTransition
in={activeMenu === "main"}
timeout={500}
classNames={{
enter: styles["menu-primary-enter"],
enterActive: styles["menu-primary-enter-active"],
exit: styles["menu-primary-exit"],
exitActive: styles["menu-primary-exit-active"],
}}
unmountOnExit
onEnter={calcHeight}
>
<div className={styles.menu}>
<DropDownItem>profile</DropDownItem>
<DropDownItem leftIcon={"icon1"} rightIcon={"icon1"} toMenu="settings">
settings
</DropDownItem>
</div>
</CSSTransition>
<CSSTransition
in={activeMenu === "settings"}
timeout={500}
classNames={{
enter: styles["menu-secondary-enter"],
enterActive: styles["menu-secondary-enter-active"],
exitActive: styles["menu-secondary-exit-active"],
}}
unmountOnExit
onEnter={calcHeight}
>
<div className={styles.menu}>
<DropDownItem toMenu="main" leftIcon={""}>
<h2>Back</h2>
</DropDownItem>
<DropDownItem leftIcon={"icon3"}>setting 1</DropDownItem>
<DropDownItem leftIcon={"icon3"}>setting 2</DropDownItem>
<DropDownItem leftIcon={"icon3"}>setting 3</DropDownItem>
</div>
</CSSTransition>
</div>
);
};
export { NavBar, NavItem, DropdownMenu };
To be concise, here is the place I think is wrong:
// !!! The wrong place
useEffect(() => {
const handleOutsideClick = (evt: MouseEvent) => {
if (!navItemRef.current?.contains(evt.target as Node)) {
setOpen(false);
}
};
document.addEventListener("click", handleOutsideClick);
return () => {
document.removeEventListener("click", handleOutsideClick);
};
}, []);