Build a Mobile Menu with Styled-Components
To take a break from the heavy lifting of the previous chapters lets address the need for a mobile friendly menu. For the sake of simplicity we will use our mobile menu for desktop as well as mobile. Onward!
We will need touch the following files in order to optimize our menu more mobile friendly.
- components/navbar.js
- components/header.js
- components/login.js
Refactor navbar.js
We’ll start by adding a NavWrapper around the Nav component:
// frontend/src/components/navbar.js
import { Link } from "gatsby"
import React from "react"
import styled from "styled-components"
import { isLoggedIn, logout } from "../services/auth"
import onClickOutside from "react-onclickoutside";
const NavWrapper = styled.div`
background-color: transparent;
border-top: 5px solid #59338f;
border-left: 2px solid #59338f;
border-bottom: 5px solid #59338f;
`
const Nav = styled.nav`
a {
color: #000000 !important;
}
ul {
li {
padding: 0.5rem 1.5rem 0.2rem 0.5rem;
border-bottom: 1px solid gray;
text-align: left;
}
li:last-child {
border-bottom: none;
}
}
`
export const Navbar = (props) => {
const { closeMenu } = props;
Navbar.handleClickOutside = () => closeMenu();
const handleLogout = () => {
logout();
closeMenu();
};
return(
<NavWrapper>
<Nav>
<ul>
<li><Link to="/">Home</Link></li>
{isLoggedIn() ? <li><Link to="/app/profile">Profile</Link></li> : ''}
{!isLoggedIn() ? <li><Link to="/app/login">Login</Link></li> : ''}
{isLoggedIn() ? <li><Link to="#" onClick={handleLogout}>Logout</Link></li> : ''}
</ul>
</Nav>
</NavWrapper>
)
}
const clickOutsideConfig = {
handleClickOutside: () => Navbar.handleClickOutside
};
export default onClickOutside(Navbar, clickOutsideConfig);
The NavWrapper is going to allow us to slide the menu in and out and we’ve refactored the Nav component to style the menu’s unordered list.
Walk through of navbar.js updates
Line 7) We are importing a new node module called react-onclickoutside. Quoting from this modules docs:
This is a React Higher Order Component (HOC) that you can use with your own React components if you want to have them listen for clicks that occur somewhere in the document, outside of the element itself (for instance, if you need to hide a menu when people click anywhere else on your page).
react-onclickoutside
But before we can import the react-onclickoutside module we need to install it.
npm install react-onclickoutside --save
Now when the user clicks anywhere outside the the navbar.js component the click event will be registered with the header.js component, (more on that later), and close the flyout menu.
Lines 9-30) Here we are restyling the menu as an unordered list.
Lines 32-38)
- We added the props argument for the Navbar.js component which will destructure a reference to the closeMenu handler, (in parent component header.js), listening for a click event outside Navbar.js which we will use to close the menu.
- handleLogout will handle logging out of the app as well as closing the menu on logout.
Lines 45-47) Adding inline javascript logic testing for the isLoggedIn state which determines which link is displayed when. In line 44 we added an onClick event handler referencing the handleLogout function.
Lines 54-58) We configure and integrate the react-onclickoutside component with our navbar.js component.
Now we can move on to refactoring the header.js component.
Refactor header.js
In the header.js file we will:
- Import useState to allow us to track the current state of the slide menu
- Import React Icons
- Add a hamburger icon for hide/showing the menu links with the component name MenuHandle
- Create styled components:
- MenuHandle
- MenuWrapper
- Refactor HeaderWrapper
// frontend/src/components/header.js
import { Link } from "gatsby"
import PropTypes from "prop-types"
import React, { useState } from "react"
import { useStaticQuery, graphql } from "gatsby"
import styled from "styled-components"
import Navbar from "./navbar"
// Import hamburger icon from react-icon library
import { MdMenu } from "react-icons/md"
// Component to wrap the hamburger icon for open/closing menu
const MenuHandle = styled.div`
position: absolute;
right: 2.0rem;
top: 1.0rem;
z-index: 10;
margin-bottom: 1.0rem;
width: 30px;
height: 30px;
font-size: 3.0rem;
cursor: pointer;
`
// Component wrapping the menu
const MenuWrapper = styled.div`
background-color: white;
position: absolute;
z-index: 5;
top: 70px;
// Depending on the state of the menu, (open/close), slide the menu in or out
right: ${(props) => (props.menuIsOpen ? '0' : '-14rem')};
// Define the transition between the open/close states of the menu
transition: ${(props) => props.menuIsOpen ? 'all 1.00s ease-out' : 'all 0.6s ease-out'};
`
// Refactor the HeaderWrapper by removing display flex
const HeaderWrapper = styled.div`
position: relative;
padding: 1.0rem 2.0rem;
width: 100%;
background-color: #9c5ea9;
a {
color: #ffffff;
text-decoration: none;
}
`
const SiteTitle = styled.div`
font-family: Arial, Helvetica, sans-serif;
`
const HeaderH1 = styled.div`
font-size: 2.5rem;
line-height: 2.6rem;
`
export const PureHeader = ({ data }) => {
// Use state to track open/close status of the menu
const [menuIsOpen, setMenuIsOpen] = useState(false);
// Handle click event to toggle state of the menu
let handleMenuClick = () => {
setMenuIsOpen(!menuIsOpen)
}
return (
<HeaderWrapper>
{/* Hamburger icon with event listener */}
<MenuHandle onClick={() => handleMenuClick()}><MdMenu /></MenuHandle>
{/* Pass menu state into MenuWrapper component */}
<MenuWrapper menuIsOpen={menuIsOpen}>
<Navbar />
</MenuWrapper>
<SiteTitle>
<HeaderH1>
<Link
to="/"
>
{data.site.siteMetadata.title}
</Link>
</HeaderH1>
</SiteTitle>
</HeaderWrapper>
)
}
export const Header = props => {
const data = useStaticQuery(graphql`
query {
site {
siteMetadata {
title
}
}
}
`)
return <PureHeader {...props} data={data}></PureHeader>
}
Header.propTypes = {
siteTitle: PropTypes.string,
}
Header.defaultProps = {
siteTitle: ``,
}
export default Header
Walk through refactoring of header.js
We are going to need graphic icons as we go along for the app including the hamburger icon which we can use now for control of the mobile menu. For icons we install the React Icons library.
cd frontend
npm install react-icons --save
Line 5) We refactored our React import to include react’s useState hook. This hook allows us to track the current open/close states of the mobile menu.
Line 11) Import the MdMenu from react-icons.
Lines 14-50) Using style-components to make the mobile menu look pretty.
Lines 34-38) Since we are using styled-components we can access the props argument listening for the the open/close states of the mobile menu and define the style accordingly.
Line 61) Define and intialize the menuIsOpen state to false.
Lines 64-66) Handle the menu click event by toggling the menuIsOpen state with the not operator.
Line 71) We add the MenuHandle component with its onClick listener and handleMenuClick handler
With the refactoring of navbar.js and header.js complete we need to rebuild the front end container:
docker-compose build frontend
Once the build is complete we can boot up the app and test the new mobile menu.
make dev
# or
docker-compose -f docker-compose.yml -f docker-compose-dev.yml up -d
Note of warning. Sometimes after a rebuild the Django server will try to connect to the postgres database before postgres is ready to accept connections causing the Django server to crash. A work-around to fix this issue is to boot postgres first, Django server second and finally the frontend.
docker-compose up -d database
docker-compose up -d server
make dev
Now we should be able to test the new menu by hitting http://localhost:8000 where by clicking on the menu hamburger icon the menu opens and closes. Also on when a menu link is clicked the menu should close.

What’s next? We need to add an additional testing utility called Enzyme that will allows us to use shallow testing on HOC components. You see, if we were to run our frontend unit tests now, the tests would fail since we implemented react-onclickoutside which is a Higher-Order Component and consequently our frontend tests fail now. But fear not, the Enzyme testing utility will allows us to work around this issue.