How to implement 'Drawer' in 'AppBar' in React MUI using Next.js framework?

68 Views Asked by At

I would like to have the App bar at the top with 'hamburger' on the left, when the hamburger is clicked , Sidenav (drawer) should appear. I am using React Material and Next.js (App router). The app bar and the drawer are suppose to be in 2 seperate components , header.tsx and sidenav.tsx respectively. The example provided in the MUI website is in single file and I am not able to implement in the 2 different components. Also the main content is suppose to be a 3rd component (dashboard).

Sidenav.ts

const drawerWidth = 240;

const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{
  open?: boolean;
}>(({ theme, open }) => ({
  flexGrow: 1,
  padding: theme.spacing(3),
  transition: theme.transitions.create('margin', {
    easing: theme.transitions.easing.sharp,
    duration: theme.transitions.duration.leavingScreen,
  }),
  marginLeft: `-${drawerWidth}px`,
  ...(open && {
    transition: theme.transitions.create('margin', {
      easing: theme.transitions.easing.easeOut,
      duration: theme.transitions.duration.enteringScreen,
    }),
    marginLeft: 0,
  }),
}));

interface AppBarProps extends MuiAppBarProps {
  open?: boolean;
}

const AppBar = styled(MuiAppBar, {
  shouldForwardProp: (prop) => prop !== 'open',
})<AppBarProps>(({ theme, open }) => ({
  transition: theme.transitions.create(['margin', 'width'], {
    easing: theme.transitions.easing.sharp,
    duration: theme.transitions.duration.leavingScreen,
  }),
  ...(open && {
    width: `calc(100% - ${drawerWidth}px)`,
    marginLeft: `${drawerWidth}px`,
    transition: theme.transitions.create(['margin', 'width'], {
      easing: theme.transitions.easing.easeOut,
      duration: theme.transitions.duration.enteringScreen,
    }),
  }),
}));

const DrawerHeader = styled('div')(({ theme }) => ({
  display: 'flex',
  alignItems: 'center',
  padding: theme.spacing(0, 1),
  // necessary for content to be below app bar
  ...theme.mixins.toolbar,
  justifyContent: 'flex-end',
}));

export default function PersistentDrawerLeft() {
  const theme = useTheme();
  const [open, setOpen] = React.useState(false);

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

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

  return (
    <Box sx={{ display: 'flex' }}>
      <CssBaseline />
      <AppBar position="fixed" open={open}>
        <Toolbar>
          <IconButton
            color="inherit"
            aria-label="open drawer"
            onClick={handleDrawerOpen}
            edge="start"
            sx={{ mr: 2, ...(open && { display: 'none' }) }}
          >
            <MenuIcon />
          </IconButton>
          <Typography variant="h6" noWrap component="div">
            Persistent drawer
          </Typography>
        </Toolbar>
      </AppBar>
      <Drawer
        sx={{
          width: drawerWidth,
          flexShrink: 0,
          '& .MuiDrawer-paper': {
            width: drawerWidth,
            boxSizing: 'border-box',
          },
        }}
        variant="persistent"
        anchor="left"
        open={open}
      >
        <DrawerHeader>
          <IconButton onClick={handleDrawerClose}>
            {theme.direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
          </IconButton>
        </DrawerHeader>
        <Divider />
        <List>
          {['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
            <ListItem key={text} disablePadding>
              <ListItemButton>
                <ListItemIcon>
                  {index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
                </ListItemIcon>
                <ListItemText primary={text} />
              </ListItemButton>
            </ListItem>
          ))}
        </List>
        <Divider />
        <List>
          {['All mail', 'Trash', 'Spam'].map((text, index) => (
            <ListItem key={text} disablePadding>
              <ListItemButton>
                <ListItemIcon>
                  {index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
                </ListItemIcon>
                <ListItemText primary={text} />
              </ListItemButton>
            </ListItem>
          ))}
        </List>
      </Drawer>
      <Main open={open}>
        <DrawerHeader />
        <Typography paragraph>
          Dashboard.tsx (3rd Component) 
         
        </Typography>
        
      </Main>
    </Box>
  );
}
2

There are 2 best solutions below

3
KcH On

I tried splitting the components in the below runnable sandbox,

Demo.tsx -> holds the header and two separate components (side drawer, main)

Code example ...

Let me know if you require any more clarity on the same

0
Md Asaduzzaman Atik On

Here, you can use the ReactContext API or third-party state management libraries (redux, recoil) Edit controll-#Drawer-state-from-outside-component#AppBar

Let's start by installing the necessary packages/libraries along with MUI. We will use recoil for quick development.

npm i recoil

Now let's wrap our project's root component/index.{js,ts} with RecoilRoot to take the advantage of global state manipulation

Note: Not necessarily the root component. Components that use recoil state, need RecoilRoot to appear somewhere in the parent tree. A good place to put this is in your root component.

First import RecoilRoot from recoil then wrap the root component with RecoilRoot (example)

// react
import * as React from "react";
import * as ReactDOM from "react-dom/client";

// ...other imports

// recoil
import { RecoilRoot } from "recoil";

// custom
import App from "./App";

// ...other typings, interfaces, variables, functions, and more

const rootElement = document.getElementById("root");
const root = ReactDOM.createRoot(rootElement!);

root.render(
  <RecoilRoot>

    // ...other wrappers/components

      <App />

    // ...other wrappers/components

  </RecoilRoot>
);

Now let's make a recoil atom for drawer open state management (example)

It's kind of react's useState. But in the case of useState, useState manages the state within a component, and on the other hand Recoil atoms are global, accessible, and updateable from anywhere in our React app.

// recoil
import { atom } from "recoil";

export type DrawerStateProps = boolean;

export const drawerStateDefaultValue: DrawerStateProps = false;

export const drawerState = atom<DrawerStateProps>({
  key: "drawerState",
  default: drawerStateDefaultValue,
});

Now, components that need to read from and write to an atom should use recoil's useRecoilState() hook to manipulate states across the app, globally.

Like in the component with <Drawer />, it just needs the state value to open and close the drawer properly (example)

// react
import * as React from "react";

// ...other imports

import Drawer from "@mui/material/Drawer";

// ...other imports

// recoil
import { useRecoilState } from "recoil";
import { drawerState } from "../lib/recoil/atoms/drawerAtoms";

const SideDrawer = () => {
  const [drawerOpen, setDrawerOpen] = useRecoilState(drawerState); // Getting the state's realtime value from the recoil atom [getter, setter]

  // ...other typings, interfaces, variables, functions, and more

  const DrawerList = /* List component */;

  return (
    <Drawer open={drawerOpen} {/* ...rest props */}>
      {DrawerList}
    </Drawer>
  );
};

export default SideDrawer;

And the component with <AppBar />, where we just update the value for drawerState atom to control the <Drawer /> opening and closing (example)

// react
import * as React from "react";

// @mui
import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";

// ...other imports

import IconButton from "@mui/material/IconButton";

// @mui/icons-material
import MenuIcon from "@mui/icons-material/Menu";

// recoil
import { useRecoilState } from "recoil";
import { drawerState } from "../shared/lib/recoil/atoms/drawerAtoms";

// custom
import SideDrawer from "../shared/components/SideDrawer";

export default function Index() {
  const [drawerOpen, setDrawerOpen] = useRecoilState(drawerState); // Getting the state's realtime value from the recoil atom [getter, setter]
  const toggleDrawer = (newOpen: boolean) => () => {
    setDrawerOpen(newOpen);
  };
  return (
    <Box>
      <Box sx={{ flexGrow: 1 }}>
        <AppBar position="static">
          <Toolbar>
            <IconButton

              {/* ...more useful props*/}

              onClick={toggleDrawer(true)}
            >
              <MenuIcon />
            </IconButton>

            // ...other decoration components

          </Toolbar>
        </AppBar>
        <SideDrawer />
      </Box>
    </Box>
  );
}

That's all.

Thank You <3