react logo

Diving Deep into useState(), useEffect(), and useRef(): A simple mobile menu with React

Creating a simple mobile menu to demonstrate the functionality of 3 core React hooks

React is an extremely powerful tool for executing JavaScript in a more efficient way, while abstracting away repetitive scripting. React offers several hooks that allow its users to quickly spin up readable and maintainable code.

In this project, we will utilize the three most essential core hooks: useState(), useEffect(), and useRef() to build a simple mobile dropdown menu.

See this component in action on my portfolio site!

React Hooks Overview

For our mobile menu component, we will be using three hooks. Let’s dive deeper into each of them below.

useState() manages state changes

Where we might normally store DOM elements in variables and add event listeners to manage state in vanilla JS, React abstracts away this process into a considerably simpler syntax.

It provides a variable to store the current state of a variable, and an updater function to update the state.

The syntax for useState() is shown below:

JavaScript
const [ state, setState ] = useState(initialState);

We use array deconstruction to name two variables that are returned from the useState() hook: our state variable, and our state updater function. We then set our initial state by passing a value to the useState() function.

To demonstrate how we can call the state updater function, let’s imagine in our return block of jsx, we have a button that will update the state when clicked or an input field that will update the state any time it is changed:

JavaScript
return (
  <button onClick={() => setState(2)}>Set to 2!</button> //Sets state to 2 any time it is clicked.
  <button onClick={() => setState(!state)}>Toggle State!</button> //Toggles state if using boolean values.
  <input
    type="text"
    value={state}
    onChange={(e) => setState(e.target.value)} //Sets state to whatever the user inputs in the text field.
  />
)

A quick note: Make sure you wrap your state updater function in a callback function if you are updating the state based on a previous value (like in the last example).

useEffect() manages side effects in a predictable way

Since React components are designed to be pure functions, useEffect() allows us to manage side effects in a way that is predictable. React can predictably handle anything within a component that deals only with itself, but when we need to interact with something outside of the scope of our component, we need to use useEffect() as a sort-of “messenger” to the “outside world.” This is because our side effect probably doesn’t need to be run every time its parent component re-renders (Like fetching from an API every time your component changes).

Let’s break down the syntax below:

JavaScript
useEffect(() => {
  //Do something outside the scope of our component here:
  //console.log(myState);
  //API fetching
  //DOM manipulation
  return () => cleanupFunction();
}, [myState])

useEffect() takes two arguments: a function and a dependency array. If the dependency array is empty, [], then the function will run only once when the component is first mounted, and never again unless the component unmounts and mounts again. The dependency array holds one or more state variables that trigger the useEffect() function to run every time their state changes.

The function should also return a cleanup function removing any event listeners to prevent duplicate event listeners being added each time the effect is run.

Note: If you don’t include the second argument at all (no brackets), then the function will rerun on every render, leading to unexpected results.

useRef() creates a persistent reference throughout the render cycle

In vanilla JS, we might assign a DOM element to a variable in order to do something with it, but using this method inside a React component can present some issues. Namely, the variable can only be defined once the element it’s assigned to is rendered. If the variable assignment happens before the element is mounted, it will be assigned undefined. React provides a way to not only solve that issue, but make references to the DOM more efficient.

useRef() holds the value of null until its reference element is mounted, so it won’t unexpectedly be undefined. useRef() also creates a persistent reference in React, meaning that it doesn’t trigger a re-render each time its reference is used (unlike assigning a variable directly to document.getElementById()). It also works within React’s Virtual DOM, making it way more efficient than dealing directly with the real DOM.

Are you sold? Let’s look at the syntax below:

JavaScript
const myRef = useRef(null); //Initializes with null, later to be assigned to the DOM element. 
//...
return (
  <p ref={myRef}>My Paragraph Element</p>
)

Mobile Menu Functionality

For our mobile menu, we want a button that will open and close the menu. We also want our user to be able to click outside of the open menu to close it as well (hint hint! We will be working with elements outside the scope of our component, so we will need useEffect!).

Where will our three React hooks come into play for this component?

We will use useState() to manage whether our menu should be open or not. We will use useEffect() to add an event listener in the document to track if a user clicks outside of the open menu (in order to then close the menu). We will use useRef() to assign a reference to our entire menu container (so that if the user clicks anywhere except the menu area, the open menu will be closed).

Complete Annotated Files

JSX
import { useState, useEffect, useRef } from "react"; //Import our React Hooks

const NavBar = () => {
  //Define our dynamic menu items
  const menu = [
    { text: "Projects", link: "https://blog.katiepardee.com" },
    { text: "Skills", link: "#skills" },
    { text: "Contact", link: "https://www.linkedin.com/in/katie-pardee-202b5b229/" }
  ]
    
  const menuCont = useRef(null); //Add a reference to our menu container
  const [isMenuOpen, setIsMenuOpen] = useState(false); //Initialize our menu state
  
  //Define operations that will happen outside the scope of this component
  useEffect(() => {
    const handleClickOutside = (e) => { //Handler Function for when a click is registered (see event listener)
      if (isMenuOpen && !menuCont.current?.contains(e.target)) { //Check if the menu is open and the click was not inside the menu
        setIsMenuOpen(false); //Close menu if both conditions are true
      }
    }

    document.addEventListener('click', handleClickOutside); //When the effect is run, add an event listener to the document

    return () => {
      document.removeEventListener('click', handleClickOutside) //Cleanup function to avoid memory leaks
      
    }
  }, [isMenuOpen]); //Dependency array tells effect to run every time the isMenuOpen state changes

  return (
    <header>
      <div ref={menuCont}>
        <div className="menu-bar">
          <h1>This Could Be Your Logo</h1>
          <button onClick={() => {setIsMenuOpen(!isMenuOpen)}}> {/* Toggles the state of isMenuOpen */}
            <svg>Your SVG Here!</svg>
          </button>
        </div>
        <nav className="mobile-menu-parent">
          <ul id="mobile-menu" className={isMenuOpen ? "open" : ""}> {/* If isMenuOpen is truthy, add class "open" */}
            {menu.map((item) => ( //Map through our menu array to create list items
              <li key={item.text} >
                <a className="menu-item" href={item.link}>{item.text}</a>
              </li>
            ))}
          </ul>
        </nav>
      </div>
    </header>
  )
}

export default NavBar;

And here is some styling for a smooth transition:

CSS
.menu-bar {
    display: flex;
    justify-content: space-between;
    padding: 20px 10px;
}

.mobile-menu-parent {
    position: relative;
}

#mobile-menu {
    position: absolute;
    width: 100%;
    max-height: 0;
    transition: max-height 400ms ease, padding 400ms ease, opacity 500ms ease;
    opacity: 0;
    background-color: rgb(105,105,105);
    display: flex;    
    flex-direction: column;
    align-items: center;
    gap: 12px;
    border-bottom: solid 4px black;
    z-index: 10;
}

#mobile-menu.open {
    max-height: 250px;
    opacity: 1;
    padding: 40px 4px;
}

.menu-item {
    color: white;
}