Search code examples
reactjstoggleresponsivesidebar

React toggle responsive sidebar based on browser width &/or user clicks


React noob here. I am creating a dashboard with three parts. Sidebar | Navbar & content below navbar. Sidebar behavior should be: open initially; auto-hide when browser-width < 498px; auto-open when browser width is increased beyond 498px; open/close when navbar toggle is hit, regardless of browser window width (ie., mobile or desktop). I know how to do this in jQuery but want to learn React way. Searching the forums and Google, I created a working model. However, I sense that the code can be simplified or at least audited for issues. Hoping for some senior enlightenment, all help is welcomed. Worried about dependency warnings and calling of 2 handlers. Did not use addEventListener as I read Safari may have issues. Notes, I avoided using css media query for auto-hide, as I could not figure out how to override css file media query of display:none from React when click component is used. Using react 17.0.2; bootstrap 5.1.3;

Dashboard.js

import { useEffect, useRef, useState } from 'react';
import "./Dashboard.css";
import Navbar from "./Navbar";
import Sidebar from "./Sidebar";

const Dashboard = () => {
const sidebarRef = useRef(null); // used to get sidebar width
const [usMobile, setMobile] = useState("");
const mq = window.matchmedia("(max-width: 498px)");
const [firsTime, setFirsTime] = useState(true);

useEffect(() => {
  //handle sidebar display clicks from Navbar
  // makes/sets initial sidebar state to open
  // returns true when window is < 498px
  // unmount cleanup handler
  toggleSidebar();
  mq.addListener(toggleSidebar);
  return () => mq.removeListener(toggleSidebar);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // warning about dependency here

useEffect(() => {
  // handles sidebar display based on resize
  // returns treu when window is < 498
  mq.addListener(hideSideBar);
  return () => mq.removeListener(hideSideBar);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // warning about dependency here

const hideSideBar = () => {
  if(mq.matches) { 
    setMobile(true);
  } else {
    setMobile(false);
  }
}

// toggle sidebar based on user clicks
const toggleSidebar = ( clicked ) => {
  let sidebarWidth = sidebarRef.current.offsetWidth;
  if (firsTime) {
     setShowSidebar(true);
     setMobile(false);
     setFirstTime(false);
  } else {
     if ( clicked = "yes") {
        if (sidebarWidth == 0){
           setShowSidebar(true);
           setMobile(false);
        } else {
           setShowSidebar(false);
        }
        clicked = "";
     }
  }
}

return (
  <span ref={sidebarRef}>
    {!isMobile && showSidebar && <Sidebar onClick={toggleSidebar} />}
  </span>
  <div className="flex-fill content-wrapper">
    <Navbar showSideBar={showSidebar} onClick={toggleSidebar} />
  </div>
);
}

export default Dashboard;

Navbar.js

import {useState} from 'react';

const Navbar = (props) => {
  // eslint-disable-next-line no-unused-vars
  // set is unused but it will not work otherwise
  const [clicked, setClicked] = useState("yes"); 

  return (
    <div className="d-flex justify-content-between bg-white py-2 ps-3 pe-4">
      <div className="col-auto">
        <button 
            className="menu-icon-btn"
            onClick={() => props.onClick(clicked)} >
        </button>
      </div>
    </div>
)
}
export default Navbar;

Sidebar.js

import {useState} from 'react';

const Sidebar = (props) => {
  const [clicked, setClicked] = useState('yes');

  return (
    <div className="d-flex flex-column flex-shrink-0 text-white bg-dark sidebar">
      <span className="fs4">Sidebar Title</span>
      <ul className="nav nav-pills flex-column mb-auto">
        <li className="nav-item">
            Home
        </li>
      </ul>
      <hr>
      <span id="toggle-x"
            className="btn btn-outline-primary border-0 inline d-md-none mx-auto mb-2">
            aria-label="Toggle Sidebar Nav"
            onClick={() => props.onClick(clicked)} >
              CLOSE
       </span>
    </div>   
  )
}

export default Sidebar;

Dashboard.css Most all css is from bootstrap; these are just app custom styles

.menu-icon-btn {
  background: none;
  border: none;
  padding: 0;
}

.toggle-icon {
  width: 32px;
  height: 32px;
  fill: var(--medium-gray);
  cursor: pointer;
}

.menu-icon {
  width: 20px;
  height: 20px;
  fill: var(--medium-gray);
  cursor: pointer;
}

.sidebar,
.content-wrapper {
  height: 100vh;
}

.sidebar-wrapper {
  width: 200px;
}
/* Omitted the following as I was unable to force it to change from within React. In jQuery(show/hide seem to override this just fine.)*/
/* @media screen and (min-width: 0px) and (max-width: 700px) {
  .sidebar-wrapper {
    display: none;
  }
} */

New Dashboard.js

const Dashboard = () => {
    const isDesktop = () => window.innerWidth > 598;
    const [sidebarStatus, setSidebarStatus] = useState("");

    useEffect(() => {
        window.addEventListener("resize", () => {
            setSidebarStatus(isDesktop());
        });
        return () => window.removeEventListener("resize", isDesktop);
    }, []);

    const toggleSidebar = (open) => {
        setSidebarStatus(open);
    };

    return (
    
        {sidebarStatus && (
          <Sidebar showSideBar={sidebarStatus} onClick={toggleSidebar} />
        )}

    )
}

New Navbar.js

const Navbar = (props) => {

return (
        <button
          id="toggle"
          className="menu-icon-btn py-2"
          data-menu-icon-btn
          onClick={() => props.onClick(!props.showSideBar)}
        > Toggle Sidebar
        </button>
)
}



Solution

  • Your code is broken.. i made a little change to your code:

    import React, { useEffect, useRef, useState } from "react";
    import "./Dashboard.css";
    import Navbar from "./Navbar";
    import Sidebar from "./Sidebar";
    
    const Dashboard = () => {
      // i don't know what is using for..
      const sidebarRef = useRef(null); // used to get sidebar width
      const [isMobile, setMobile] = useState(document.body.clientWidth <= 498);
      // use 'init' | 'open' | 'close', that you don't need remember if suer clicked
      const [sidebarStatus, setSidebarStatus] = useState("init");
    
      useEffect(() => {
        // add listener only once, or many listeners would be created every render
        const mq = window.matchMedia("(max-width: 498px)");
        mq.addListener((res) => {
          setMobile(res.matches);
        });
        return () => mq.removeListener(toggleSidebar);
      }, []);
    
      // react use status change fire effects, fire function manually is tired
      // here calculate should show sidebar
      const showSidebar =
        sidebarStatus === "open" || (!isMobile && sidebarStatus === "init");
    
      const toggleSidebar = (open) => {
        setSidebarStatus(open ? "open" : "close");
      };
    
    
      return (
        <>
          <span ref={sidebarRef}>
            {showSidebar && <Sidebar onClick={toggleSidebar} />}
          </span>
          <div className="flex-fill content-wrapper">
            <Navbar showSideBar={showSidebar} onClick={toggleSidebar} />
          </div>
        </>
      );
    };
    
    export default Dashboard;
    

    attention that toggleSidebar param from navbar and sidebar are const true and false. Do not use status in different file