I'm writing a react Header component and have added the functionality to click in and out of a dropdown menu. I'm getting the following error whenever I load the page:
next-dev.js?3515:20 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render
I imagine I'm making a mistake somewhere in a useEffect
(I am running 2 useEffects, one inside a component and one inside another function), but am unsure where I'm going wrong. Here is the code below:
import React from "react";
import { useState, useEffect, useRef } from "react";
import { FaChevronDown, FaLastfmSquare } from "react-icons/all";
import { signOut } from "firebase/auth";
import { useRouter } from "next/router";
import { auth } from "../utils/auth";
function useOutsideAlerter() {
const [visible, setVisible] = useState(false);
const ref = useRef(null);
useEffect(() => {
const handleHideDropdown = (event) => {
if (event.key === "Escape") {
setVisible(false);
}
};
const handleClickOutside = (e) => {
if (ref.current && !ref.current.contains(e.target)) {
setVisible(false);
}
};
document.addEventListener("keydown", handleHideDropdown, true);
document.addEventListener("mousedown", handleClickOutside, true);
return () => {
document.removeEventListener("keydown", handleHideDropdown, true);
document.removeEventListener("mousedown", handleClickOutside, true);
};
}, []);
return { visible, ref, setVisible };
}
const Header = () => {
const [user, setUser] = useState(null);
const router = useRouter();
const { ref, visible, setVisible } = useOutsideAlerter();
useEffect(() => {
const user = localStorage.getItem("user");
if (user) {
setUser(JSON.parse(user));
}
});
const logUserOut = () => {
signOut(auth)
.then(() => {
localStorage.removeItem("user");
router.push("/");
})
.catch((error) => {
console.error(error);
alert("There was a problem signing out.");
});
};
return (
<div className="flex items-center justify-between p-4 border-b border-gray-300">
<h1 className={"font-bold text-lg"}>Matrice</h1>
<div ref={ref}>
<button
onClick={() => setVisible(!visible)}
className="hover:bg-gray-100 rounded-md p-2 flex items-center justify-center text-gray-400"
>
{user?.displayName || "Login"} <FaChevronDown className={"ml-2"} />
</button>
{visible && (
<div
className={
"absolute top-14 right-4 rounded-md border border-gray-300 bg-white overflow-hidden"
}
>
{user && (
<button
onClick={logUserOut}
className={
"text-left text-sm hover:bg-blue-700 hover:text-white w-full px-4 py-2"
}
>
Log Out
</button>
)}
<button
onClick={() =>
router.replace(
"mailto:email"
)
}
className={
"text-left text-sm hover:bg-blue-700 hover:text-white w-full px-4 py-2"
}
>
Request Feature
</button>
</div>
)}
</div>
</div>
);
};
export default Header;
CodePudding user response:
I imagine I'm making a mistake somewhere in a
useEffect
Yes, you haven't specified a dependency array for your useEffect()
within your Header
component:
useEffect(() => {
const user = localStorage.getItem("user");
if (user) {
setUser(JSON.parse(user));
}
}, []); // <--- added empty dependency array
Without a dependeency array, your useEffect()
callback will run after every render/rerender. So in your current code, this happens:
- React renders your JSX
- Your
useEffect()
callback is called, callingsetUser
if theuser
key in your local storage is set - As
setUser()
was called with a new object reference (a new object is created when doingJSON.parse()
), React sees this as an update and triggers a rerennder, and so we go back to step 1 above, causing an infinite loop.
By adding an empty dependency array []
to the useEffect()
call, you're telling React to only call your function on the initial mount, and not subsequent rerenders.