Search code examples
reactjsaddeventlistener

React stop click outside event


I have 2 components. A Navbar and a Sidebar/Cart. On the Navbar i have a button that opens and closes the Sidebar. I have an event that triggers the clickOutside function and its working. The issue is when i click the cart item when the sidebar is active, the sidebar closes and reopens again.

My App.jsx:

import { useEffect, useState } from "react";
import "./App.css";
import { Navbar } from "./Components/Navbar";
import { ListItem } from "./Components/ListItem";
import { Cart } from "./Components/Cart";

function App() {
  const [list, setList] = useState([]);
  const [cartActive, setCartActive] = useState(false);

  const fetchList = () => {
    fetch(`https://fakestoreapi.com/products`)
      .then((res) => res.json())
      .then((data) => {
        setList(data);
      })
      .catch((error) => {});
  };

  useEffect(() => {
    fetchList();
  }, []);

  return (
    <>
      <Navbar setCartActive={setCartActive} cartActive={cartActive} />
      <Cart cartActive={cartActive} setCartActive={setCartActive} />
      <div className="main">
        <div className="container">
          {list.map((item) => {
            return <ListItem key={item.id} item={item} />;
          })}
        </div>
      </div>
    </>
  );
}

export default App;
 

My Navbar:

import { useEffect, useState } from "react";
import { FaShoppingBag } from "react-icons/fa";
import { useSelector } from "react-redux";

export const Navbar = ({ setCartActive, cartActive }) => {
  const cart = useSelector((state) => state.cart.cart);
  const [totalItems, setTotalItems] = useState(0);

  const fetchTotalItems = () => {
    setTotalItems(cart.reduce((acc, curr) => acc + curr.amount, 0));
  };

  useEffect(() => {
    fetchTotalItems();
  }, [cart]);

  return (
    <nav>
      <div className="container">
        <div className="brand">
          Redux<span>Store</span>
        </div>
        <div className="icon" onClick={() => setCartActive(!cartActive)}>
          <FaShoppingBag /> ({totalItems})
        </div>
      </div>
    </nav>
  );
};

And my Sidebar/Cart:

import { useSelector } from "react-redux";
import { CartItem } from "./CartItem";
import { useEffect, useRef } from "react";

export const Cart = ({ cartActive, setCartActive }) => {
  const cart = useSelector((state) => state.cart.cart);
  const cartRef = useRef();

  const handleClickOutside = (e) => {
    if (cartActive && cartRef.current && !cartRef.current.contains(e.target)) {
      setCartActive(false);
    }
  };

  useEffect(() => {
    document.addEventListener("mousedown", handleClickOutside);

    () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  });

  return (
    <div className={`cart ${cartActive ? "active" : ""}`} ref={cartRef}>
      {cart.map((item) => {
        return <CartItem key={item.id} item={item} />;
      })}
    </div>
  );
};

This works but when the user clicks on the cart icon when the sidebar is open the sidebar closes and reopens it. this is because it catches the clickOutside function. how can i stop that clickoutside function on the navbar?

here is a link to codesandbox


Solution

  • The issue is you are changing the active state from both Sidebar and Navbar components. So when you click on the icon in the navbar, first handleClickOutside function is triggered, which closes the sidebar and immediately onClick function in Navbar component is triggered which opens it.

    One quick solution is exclude the Navbar button in the handleClickOutside function.

    const handleClickOutside = (e) => {
        if (
          isActive &&
          sidebarRef.current &&
          !sidebarRef.current.contains(e.target) &&
          e.target !== document.getElementsByClassName("icon")[0]
        ) {
          setIsActive(false);
        }
    };
    

    Check this CodeSandbox demo. In the demo I also changed the mousedown event to click event.