Home > Software engineering >  How to persist the open state of my Drawer in Material-UI?
How to persist the open state of my Drawer in Material-UI?

Time:10-09

I would like to set up a sidebar with the possibility to collapse it to have only the sidebar icons.

I use Material-UI, I managed to set up the basic sidebar (Mini Variant Drawer) and it works fine. However, I can't keep the state (boolean) when I change the page or when I refresh the page I'm on.

Does anyone know how I can do this?

function:

const [open, setOpen] = useState(true)

const handleDrawerOpen = () => {
  setOpen(true);
};

const handleDrawerClose = () => {
  setOpen(false);
};

Full code:

import React, { useState } from "react";
import clsx from "clsx";
import { makeStyles } from "@material-ui/core/styles";
import Drawer from "@material-ui/core/Drawer";
// import List from '@material-ui/core/List';
import Divider from "@material-ui/core/Divider";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import { Link as Route, NavLink, withRouter } from "react-router-dom";
import styled, { createGlobalStyle } from "styled-components/macro";
import {
  Chip,
  Collapse,
  List as MuiList,
  ListItemIcon,
  Tooltip,
  Typography,
} from "@material-ui/core";
import { darken, rgba } from "polished";
import { ExpandLess, ExpandMore } from "@material-ui/icons";
import PropTypes from "prop-types";
import {
  sidebarMainRoutes as mainRoutes,
  sidebarShortRoutes as shortRoutes,
} from "../../routes";
import ReactRouterPropTypes from "react-router-prop-types";
import desktopLogo from "../../assets/images/logos/data_is_logo.png";
import mobileLogo from "../../assets/images/logos/data_is_logo_icon.png";
import { CertificationIcon, LogOutIcon } from "../Icons/Icons";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import CertificationModal from "../../pages/CertificationPage/CertificationModal";
import LogOutModal from "../../pages/AuthPages/components/LogOutModal";

const drawerWidth = 240;

const GlobalStyle = createGlobalStyle`
  .MuiListItem-gutters {
    padding-left: 8px !important;
    padding-right: 10px !important;
  }
`;

const useStyles = makeStyles((theme) => ({
  root: {
    display: "flex",
  },
  appBar: {
    zIndex: theme.zIndex.drawer   1,
    transition: theme.transitions.create(["width", "margin"], {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.leavingScreen,
    }),
  },
  appBarShift: {
    marginLeft: drawerWidth,
    width: `calc(100% - ${drawerWidth}px)`,
    transition: theme.transitions.create(["width", "margin"], {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.enteringScreen,
    }),
  },
  menuButton: {
    marginRight: 36,
  },
  hide: {
    display: "none",
  },
  drawer: {
    width: drawerWidth,
    flexShrink: 0,
    whiteSpace: "nowrap",
    borderRight: "1px solid #F4F4F4",
  },
  drawerOpen: {
    width: drawerWidth,
    borderRight: "1px solid #F4F4F4",
    transition: theme.transitions.create("width", {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.enteringScreen,
    }),
  },
  drawerClose: {
    borderRight: "1px solid #F4F4F4",
    transition: theme.transitions.create("width", {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.leavingScreen,
    }),
    overflowX: "hidden",
    width: theme.spacing(7)   1,
    [theme.breakpoints.up("sm")]: {
      width: theme.spacing(9)   20,
    },
  },
  toolbar: {
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
    padding: theme.spacing(0, 1),
    // necessary for content to be below app bar
    ...theme.mixins.toolbar,
  },
}));

const List = styled(MuiList)`
  background-color: ${(props) => props.theme.sidebar.background};
  padding: ${(props) => props.theme.spacing(2)}px !important;
`;

const Items = styled.div`
  padding-top: ${(props) => props.theme.spacing(2.5)}px;
  padding-bottom: ${(props) => props.theme.spacing(2.5)}px;
`;

const Category = styled(ListItem)`
  padding-top: ${(props) => props.theme.spacing(3)}px;
  padding-bottom: ${(props) => props.theme.spacing(3)}px;
  padding-left: ${(props) => props.theme.spacing(8)}px;
  padding-right: ${(props) => props.theme.spacing(7)}px;
  font-weight: ${(props) => props.theme.typography.fontWeightRegular};
  border-radius: 10px !important;

  svg {
    color: ${(props) => props.theme.sidebar.color};
    font-size: 20px;
    width: 20px;
    height: 20px;
  }

  &:hover {
    background-color: ${(props) =>
      props.theme.palette.customer.blue} !important;
    color: ${(props) => props.theme.palette.primary.contrastText} !important;
    border-radius: 10px !important;

    svg {
      color: ${(props) => props.theme.palette.common.white};
    }

    span {
      color: ${(props) => props.theme.palette.common.white};
    }
  }

  &.${(props) => props.activeClassName} {
    background-color: ${(props) =>
      darken(0.03, props.theme.palette.customer.blue)};
    color: ${(props) => props.theme.palette.primary.contrastText} !important;

    svg {
      color: ${(props) => props.theme.palette.common.white};
    }

    span {
      color: ${(props) => props.theme.palette.common.white};
    }
  }
`;

const CategoryText = styled(ListItemText)`
  margin: 0;

  span {
    color: ${(props) => props.theme.sidebar.color};
    font-size: ${(props) => props.theme.typography.body1.fontSize}px;
  }
`;

const CategoryIconLess = styled(ExpandLess)`
  color: ${(props) => rgba(props.theme.sidebar.color, 0.5)};
`;

const CategoryIconMore = styled(ExpandMore)`
  color: ${(props) => rgba(props.theme.sidebar.color, 0.5)};
`;

const Link = styled(ListItem)`
  padding-left: ${(props) => props.theme.spacing(17.5)}px;
  padding-top: ${(props) => props.theme.spacing(2)}px;
  padding-bottom: ${(props) => props.theme.spacing(2)}px;

  span {
    color: ${(props) => rgba(props.theme.sidebar.color, 0.7)};
  }

  &:hover span {
    color: ${(props) => rgba(props.theme.sidebar.color, 0.9)};
  }

  &:hover {
    background-color: ${(props) =>
      darken(0.015, props.theme.sidebar.background)};
  }

  &.${(props) => props.activeClassName} {
    background-color: ${(props) =>
      darken(0.03, props.theme.sidebar.background)};

    span {
      color: ${(props) => props.theme.sidebar.color};
    }
  }
`;

const LinkText = styled(ListItemText)`
  color: ${(props) => props.theme.sidebar.color};

  span {
    font-size: ${(props) => props.theme.typography.body1.fontSize}px;
  }

  margin-top: 0;
  margin-bottom: 0;
`;

const LinkIcon = styled(ListItemIcon)`
  color: ${(props) => props.theme.sidebar.color};
`;

const LinkBadge = styled(Chip)`
  font-size: 11px;
  font-weight: ${(props) => props.theme.typography.fontWeightBold};
  height: 20px;
  position: absolute;
  right: 28px;
  top: 8px;
  background: ${(props) => props.theme.sidebar.badge.background};

  span.MuiChip-label,
  span.MuiChip-label:hover {
    cursor: pointer;
    color: ${(props) => props.theme.sidebar.badge.color};
    padding-left: ${(props) => props.theme.spacing(2)}px;
    padding-right: ${(props) => props.theme.spacing(2)}px;
  }
`;

const CategoryBadge = styled(LinkBadge)`
  top: 12px;
`;

const SidebarSection = styled(Typography)`
  color: ${(props) => props.theme.sidebar.color};
  padding: ${(props) => props.theme.spacing(4)}px
    ${(props) => props.theme.spacing(4)}px
    ${(props) => props.theme.spacing(2)}px;
  opacity: 0.9;
  display: block;
  font-size: 20px !important;
`;

const Brand = styled(ListItem)`
  font-size: ${(props) => props.theme.typography.h5.fontSize};
  font-weight: ${(props) => props.theme.typography.fontWeightMedium};
  color: ${(props) => props.theme.sidebar.header.color};
  background-color: ${(props) => props.theme.sidebar.header.background};
  font-family: ${(props) => props.theme.typography.fontFamily};
  min-height: 56px;
  padding-left: ${(props) => props.theme.spacing(6)}px;
  padding-right: ${(props) => props.theme.spacing(6)}px;
  justify-content: center;
  cursor: pointer;

  ${(props) => props.theme.breakpoints.up("sm")} {
    min-height: 64px;
  }

  &:hover {
    background-color: ${(props) => props.theme.sidebar.header.background};
  }
`;

const StyledDrawer = styled(Drawer)`
  ::-webkit-scrollbar {
    width: 6px;
  }

  ::-webkit-scrollbar-track {
    box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  }

  ::-webkit-scrollbar-thumb {
    background-color: ${(props) => props.theme.palette.primary.main};
    outline: 1px solid ${(props) => props.theme.palette.primary.main};
  }
`;

const SidebarCategory = (props) => {
  const { name, icon, isOpen, isCollapsable, badge, ...rest } = props;
  return (
    <Category {...rest}>
      <Tooltip title={name} placement="right">
        <ListItemIcon>{icon}</ListItemIcon>
      </Tooltip>
      <CategoryText>{name}</CategoryText>
      {isCollapsable ? (
        isOpen ? (
          <CategoryIconMore />
        ) : (
          <CategoryIconLess />
        )
      ) : null}
      {badge ? <CategoryBadge label={badge} /> : ""}
    </Category>
  );
};

const SidebarLink = ({ name, to, badge, icon }) => {
  return (
    <Link
      button
      dense
      component={NavLink}
      exact
      to={to}
      activeClassName="active"
    >
      <Tooltip title={name} placement="right">
        <LinkIcon>{icon}</LinkIcon>
      </Tooltip>
      <LinkText>{name}</LinkText>
      {badge ? <LinkBadge label={badge} /> : ""}
    </Link>
  );
};

const Sidebar = ({
  onDrawerChange,
  staticContext,
  location,
  addModal,
  addLogOutModal,
  ...rest
}) => {
  const initOpenRoutes = () => {
    /* Open collapse element that matches current url */
    const pathName = location.pathname;

    let _routes = {};

    mainRoutes.forEach((route, index) => {
      const isActive = pathName.indexOf(route.path) === 0;
      const isOpen = route.open;
      const isHome = route.containsHome && pathName === "/";

      _routes = Object.assign({}, _routes, {
        [index]: isActive || isOpen || isHome,
      });
    });

    return _routes;
  };

  const [openRoutes, setOpenRoutes] = useState(() => initOpenRoutes());
  const classes = useStyles();
  const [open, setOpen] = React.useState(true);

  const toggle = (index) => {
    // Collapse all elements
    Object.keys(openRoutes).forEach(
      (item) =>
        openRoutes[index] ||
        setOpenRoutes((openRoutes) =>
          Object.assign({}, openRoutes, { [item]: false })
        )
    );

    // Toggle selected element
    setOpenRoutes((openRoutes) =>
      Object.assign({}, openRoutes, { [index]: !openRoutes[index] })
    );
  };

  const handleDrawerOpen = () => {
    setOpen(true);
  };

  const handleDrawerClose = () => {
    setOpen(false);
  };

  const [isOpen, setIsOpen] = useState(false);
  const [isOpenLogoutDialog, setIsOpenLogoutDialog] = useState(false);
  const handleDialogCertificationOpen = () => {
    setIsOpen(true);
  };
  const handleDialogCertificationClose = () => {
    setIsOpen(false);
  };

  const handleDialogLogOutOpen = () => {
    setIsOpenLogoutDialog(true);
  };
  const handleDialogLogOutClose = () => {
    setIsOpenLogoutDialog(false);
  };

  return (
    <React.Fragment>
      <GlobalStyle />
      <CertificationModal
        isOpen={isOpen}
        handleClose={handleDialogCertificationClose}
        handleModalAction={() => console.log("Handle Modal Action")}
      />
      <LogOutModal
        isOpen={isOpenLogoutDialog}
        handleClose={handleDialogLogOutClose}
        handleModalAction={() => console.log("Déconnexion")}
      />
      <StyledDrawer
        variant="permanent"
        className={clsx(classes.drawer, {
          [classes.drawerOpen]: open,
          [classes.drawerClose]: !open,
        })}
        classes={{
          paper: clsx({
            [classes.drawerOpen]: open,
            [classes.drawerClose]: !open,
          }),
        }}
        {...rest}
      >
        <Brand>
          {open === true ? (
            <Route to="/">
              <img src={desktopLogo} alt="" width={180} />
            </Route>
          ) : (
            <Route to="/">
              <img src={mobileLogo} alt="" width={35} />
            </Route>
          )}
        </Brand>
        <Divider />
        <List disablePadding>
          <Items>
            {mainRoutes.map((category, index) => (
              <React.Fragment key={index}>
                {category.children && category.icon ? (
                  <React.Fragment key={index}>
                    <SidebarCategory
                      isOpen={!openRoutes[index]}
                      isCollapsable={true}
                      name={category.id}
                      icon={category.icon}
                      button={true}
                      onClick={() => toggle(index)}
                    />

                    <Collapse
                      in={openRoutes[index]}
                      timeout="auto"
                      unmountOnExit
                    >
                      {category.children.map((route, index) => (
                        <SidebarLink
                          key={index}
                          name={route.name}
                          to={route.path}
                          icon={route.icon}
                          badge={route.badge}
                        />
                      ))}
                    </Collapse>
                  </React.Fragment>
                ) : category.icon ? (
                  <SidebarCategory
                    isCollapsable={false}
                    name={category.id}
                    to={category.path}
                    activeClassName="active"
                    component={NavLink}
                    icon={category.icon}
                    exact
                    button
                    badge={category.badge}
                  />
                ) : null}
              </React.Fragment>
            ))}
          </Items>
        </List>
        <Divider />
        {open && <SidebarSection>Raccourcis</SidebarSection>}
        <List disablePadding>
          <Items>
            {shortRoutes.map((category, index) => (
              <React.Fragment key={index}>
                {category.children && category.icon ? (
                  <React.Fragment key={index}>
                    <SidebarCategory
                      isOpen={!openRoutes[index]}
                      isCollapsable={true}
                      name={category.id}
                      icon={category.icon}
                      button={true}
                      onClick={() => toggle(index)}
                    />

                    <Collapse
                      in={openRoutes[index]}
                      timeout="auto"
                      unmountOnExit
                    >
                      {category.children.map((route, index) => (
                        <SidebarLink
                          key={index}
                          name={route.name}
                          to={route.path}
                          icon={route.icon}
                          badge={route.badge}
                        />
                      ))}
                    </Collapse>
                  </React.Fragment>
                ) : category.icon ? (
                  <SidebarCategory
                    isCollapsable={false}
                    name={category.id}
                    to={category.path}
                    activeClassName="active"
                    component={NavLink}
                    icon={category.icon}
                    exact
                    button
                    badge={category.badge}
                  />
                ) : null}
              </React.Fragment>
            ))}
            <SidebarCategory
              icon={<CertificationIcon />}
              name="Ajouter attestation"
              onClick={handleDialogCertificationOpen}
            />
            <SidebarCategory
              icon={<LogOutIcon />}
              name="Déconnexion"
              onClick={handleDialogLogOutOpen}
            />
            {open === true ? (
              <SidebarCategory
                icon={<ChevronLeftIcon />}
                name="Réduire le menu"
                onClick={handleDrawerClose}
              />
            ) : (
              <SidebarCategory
                icon={<ChevronRightIcon />}
                name="Agrandir le menu"
                onClick={handleDrawerOpen}
              />
            )}
          </Items>
        </List>
      </StyledDrawer>
    </React.Fragment>
  );
};

export default withRouter(Sidebar);

SidebarCategory.propTypes = {
  name: PropTypes.string,
  icon: PropTypes.node,
  isOpen: PropTypes.bool,
  isCollapsable: PropTypes.bool,
  badge: PropTypes.node,
};

SidebarLink.propTypes = {
  name: PropTypes.string,
  to: PropTypes.string,
  badge: PropTypes.node,
  icon: PropTypes.node,
};

Sidebar.propTypes = {
  onDrawerChange: PropTypes.func,
  classes: PropTypes.node,
  staticContext: PropTypes.node,
  location: ReactRouterPropTypes.location,
  addModal: PropTypes.func,
  addLogOutModal: PropTypes.func,
};

CodePudding user response:

In order to persist some state across pages, you'll need to lift up your state to a common ancestor. So, if you're using Sidebar on several pages, you'll need to set the open state in the component which renders your page components, and pass the state down from there.

In order to persist state between page reloads, you can use localStorage or a cookie.

CodePudding user response:

For simple data like a boolean state, you can use local storage to persist it:

const drawerOpenKey = 'drawerOpen';
const defaultOpen = localStorage.getItem(drawerOpenKey) === 'true';
const [open, setOpen] = React.useState(defaultOpen);

React.useEffect(() => {
  localStorage.setItem(drawerOpenKey, open);
}, [open]);

Live Demo

  • Related