DEV Community

Cover image for Let's Make a Redux + Material UI + OAuth Starter template! Pt 4. Navbar & Theme Switch
Ethan Soo Hon
Ethan Soo Hon

Posted on

Let's Make a Redux + Material UI + OAuth Starter template! Pt 4. Navbar & Theme Switch

In the last part of this tutorial series we will set up a mobile responsive Navbar component that has a theme switcher and the user's profile details once they're logged in.

AppBar


Appbar docs

Material UI Docs

On this same documentation page they have many examples of built NavBars; we will take the sample code for the App Bar with a primary search field example and modify it to suit our needs.The good news is that we finished setting up the redux store in the previous article (so we don't have to add any additional code); the bad news is the NavBar code seems rather complex at first glance, so let's walk through it together!


AppBar with search

Our starting point

Breaking down our NavBar.js

1) Styles:
From our example code we copied from the documentation we see that that this particular AppBar has a search bar and a hamburger icon on the far left. We don't need the search bar or the hamber icon so we will remove the styles for those and add in additional styles for...

  • theme switch (themeToggle)
  • image container for the user's photo (imageContainer)
  • nav ruleset to change the backgroundColor. (nav)

const useStyles = makeStyles((theme) => ({
  grow: {
    flexGrow: 1,
  },
  title: {
    display: 'none',
    [theme.breakpoints.up('sm')]: {
      display: 'block',
    },
  },
  sectionDesktop: {
    display: 'none',
    [theme.breakpoints.up('md')]: {
      display: 'flex',
    },
  },
  sectionMobile: {
    display: 'flex',
    [theme.breakpoints.up('md')]: {
      display: 'none',
    },
  },
    nav: {
        backgroundColor: theme.palette.primary.main,
        color: theme.palette.primary.contrastText
    },
    themeToggle: {
        [theme.breakpoints.up('sm')]: {
            padding: 0
        }
    },
    imageContainer: {
        maxWidth: '100%',
        height: 'auto',
        '& img': {
            width: '2em'
        }
    }
}));

Enter fullscreen mode Exit fullscreen mode
We removed inputInput, inputRoot , searchIcon, search and menuButton properties


The rest is fairly simple; the syntax of the Material UI makeStyles hook allows you to easily set nested properties (Like in SASS/SCSS) AND set up media queries. We didn't explicitly set our own breakpoints when making a custom theme so they are inherited from the default theme.


Breakpoint docs

Default breakpoint values

2) Local State and Theme Switch

There are lots of state variables and functions in the example component; let's determine what they do.

Note: The example also had a submenu on desktop as well, but I chose to remove it to simplify the code. This means we deleted the following...

  • const [anchorEl, setAnchorEl] = React.useState(null);
  • const isMenuOpen = Boolean(anchorEl);
  • handleProfileMenuOpen,handleProfileMenuClose functions
  • renderMenu variable
const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = React.useState(null); 
Enter fullscreen mode Exit fullscreen mode

To enable mobile responsiveness we will have a submenu pop up when the width of the device is too small to fit all items in the default nav bar. This will be done with the Material UI Menu component. The prop anchorEl (which takes in a DOM node) determines where the menu will appear on the screen.

When the user clicks on our mobile menu icon handleMobileMenuOpen will be called. We have a variable setup to coerce the value of mobileMoreAnchorEl to a boolean. If it's still the default null value this will evaluate to false. If there is a DOM element in mobileMoreAnchorEl then we know they clicked it and want to open the mobile menu.

    const isMobileMenuOpen = Boolean(mobileMoreAnchorEl);

    /*The anchor pieces of state need to either be null or have a DOM element */
    const handleMobileMenuOpen = (event) => {
        setMobileMoreAnchorEl(event.currentTarget);
    };
    const handleMobileMenuClose = () => {
        setMobileMoreAnchorEl(null);
    };
Enter fullscreen mode Exit fullscreen mode
To close it we just set state back to null


renderMobileMenu

This variable contains the JSX for our sub-menu on mobile; The menu is made with the Material UI menu component. We will mostly leave this menu as-is but feel free to play with the Icon / badge content to change the # of notifications or emails etc...

The one thing we want to add here is a toggle to switch the theme; luckily Material UI also has a component for that


Switch docs

Just what we need!




The basic example is sufficient enough we just have to supply a few props. The most important being the checked boolean and the onChange function.

For the switch I decided true=dark mode (no particular reason) so we reach in to the redux store with useSelector and grab our theme object. If the theme.palette type is "dark" checked is true. onChange when clicked will dispatch our toggleTheme action creator we made in article 3 and voila we have a working theme button!

    const { auth, theme } = useSelector((state) => state);
const renderMobileMenu = (
        <Menu
            anchorEl={mobileMoreAnchorEl}
            anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
            id={mobileMenuId} 
            keepMounted
            transformOrigin={{ vertical: 'top', horizontal: 'right' }}
            open={isMobileMenuOpen}
            onClose={handleMobileMenuClose}
        >
            <MenuItem>
                <IconButton aria-label='show 1 new mails' color='inherit'>
                    <Badge badgeContent={1} color='secondary'>
                        <MailIcon />
                    </Badge>
                </IconButton>
                <p>Messages</p>
            </MenuItem>
...
...
...
<MenuItem>
                <IconButton aria-label='toggle dark mode'>
                    <Switch
                        color='default'
                        checked={theme.palette.type === 'dark'}
                        onChange={() => dispatch(toggleTheme())}
                        inputProps={{ 'aria-label': 'primary checkbox' }}
                        name='themeToggle'
                    ></Switch>
                </IconButton>
                <p>Theme </p>
            </MenuItem>
    </Menu>
Enter fullscreen mode Exit fullscreen mode
id here is just a string; we use it for ARIA support. Our mobile button to bring up the submenu should have an aria-controls prop equal to this ID


3) The returned JSX
Again we are mostly keeping the code from App Bar with a primary search field the only thing we add to the Navbar is the users profile picture, the switch for toggling the theme and conditional rendering dependent on the user's login status.

Here are the components wrapping the navbar...

return (
 <header className={classes.grow}>
  <AppBar position='static' component='div'>
   <Toolbar component='nav' className={classes.nav}>
    <Typography className={classes.title} variant='h6' noWrap>
     Google Oauth Redux
    </Typography>
    <div className={classes.grow} />
    ...
    ...
    ...
)
Enter fullscreen mode Exit fullscreen mode

Within the above components we have the following two divs that separate the content that will render in the navbar on desktop widths and mobile widths. We add the conditional rendering in there.
Note: The JSX in the sectionMobile div is JUST FOR THE ICON/BUTTON to open the submenu (see renderMobileMenu variable)

Desktop NavBar items
    <div className={classes.sectionDesktop}>
    {auth.user ? <>
        /* ...Mail & Notification IconButtons */
          <IconButton aria-label='toggle dark mode'>
           <Switch                               
                  color='default'                                        
                  checked={theme.palette.type === 'dark'}                                    
                  onChange={() => dispatch(toggleTheme())}                               
                  inputProps={{ 'aria-label': 'primary checkbox' }}                                      
                  name='themeToggle'                                
           </Switch>                
          </IconButton>
          <IconButton
              edge='end'                         
              aria label='account of current user'                   
              aria-haspopup='true'                               
              color='inherit'
          >
         <div className={classes.imageContainer}>                                
               <img src={auth.user.imageUrl} alt={auth.user.givenName} />                                    
             </div> 
          </IconButton>
       </> : <p>Not Logged in </p>}
      <div/>
Enter fullscreen mode Exit fullscreen mode
Mobile NavBar items
import MoreIcon from '@material-ui/icons/MoreVert';

<div className={classes.sectionMobile}>
  {auth.user ? <>
    <IconButton                                  
       aria-label='show more'                                
       aria-controls={mobileMenuId}                              
       aria-haspopup='true'                              
       onClick={handleMobileMenuOpen}                                
       color='inherit'
      >                              
        <MoreIcon />                         
      </IconButton>
  </>:  <p>Not Logged in </p>}

</div>
Enter fullscreen mode Exit fullscreen mode

Finally at the very end we throw in the variable renderMobileMenu (it's not a function, just JSX) because the menu is always being rendered (even if we're not on mobile or have not opened it) but only visible to us when we click the button that triggers a state change and causes the open prop be true.

<header>
   <AppBar> 
    <Toolbar>
    ...
    ...
    </Toolbar>
   </AppBar>
   {renderMobileMenu}
</header>
Enter fullscreen mode Exit fullscreen mode

Done 👍

If you have followed this 4 part series you should now have a very reasonable starting template for bootstrapping your front-end projects!

Here's a full working version and the repo to the completed code; please let me know what you think!

(Be sure to also read the Gotchas section in the Git repo)!

Top comments (0)