Introduction

I've made a few small apps with React and this blog is made with Gatsby (for now), so I know my way around a bit. But right around the time React introduced hooks, I was busy doing other things. The other day I thought "gee... it'd be nice to look at that hooks thing again." So I decided to dig in and build a small toggleable menu component. In this article I'll show you how both the useState and useEffect hooks work. You can see also see a finished version of the toggle on CodeSandbox.

The Markup

It often helps me to figure out what the markup for a component will be before I start to tinker with functionality. In this case, what I want is

  • a button
  • with a sibling menu
  • with a number of list items
<div>
      <button>
        Toggle
      </button>
      <ul>
        <li>
          <a href="/some-link">Some Link</a>
        </li>
        <!-- more menu items -->
      </ul>
    </div>

To turn this markup into a basic react component, we can do this -

import React from 'react'

const Toggle = () => (
  <div>
    <button>
      Toggle
    </button>
    <ul>
      <li>
        <a href="/some-link">Some Link</a>
      </li>
      <!-- more menu items -->
    </ul>
  </div>
)


export default Toggle

In terms of functionality,

  • the menu will start hidden
  • it can be opened by clicking on the button
  • it can be closed by clicking on the button, the document, or with the escape key

Believe it or not, React hooks make the functionality pretty quick to get started.

The useState Hook

As the React documentation states

useState is a Hook that lets you add React state to function components.

This means that instead of turning the component into a class, we can keep the same component as before and then add state to it.

The convention is (seems to be?) to set a variable as an array equal to useState with the initial value.

const [ state, setState ] = useState(initialValue)

In this case, state will change from the initialValue to whatever the updated state will be. setState is a function that will be changed to update the state value.

In this case, I want a toggle, so I want to make state be called open, setState will be setOpen and useState will take an initial value of false (the menu will be closed to start).

The button will then take a click handler which calls setOpen and sets its argument equal to the oposite of the initial value.

import React, { useState } from 'react'

const Toggle = () => {
    
  const [ open, setOpen ] = useState(false)
  
  return (
    <div>
      <button onClick={ () => setOpen(!open) }>
        Toggle
      </button>
      <ul>
        <li>
          <a href="/some-link">Some Link</a>
        </li>
        <!-- more menu items -->
      </ul>
    </div>
  )
}

export default Toggle

Now that I've got a true/false value set up, I can use that to manage aspects of the component.

import React, { useState } from 'react'

const Toggle = () => {
    
  const [ open, setOpen ] = useState(false)
  
  // assumes that show and active are classes for css
  return (
    <div>
      <button 
        aria-expanded={open === true ? "true" : "false"}
        className={open === true ? "active" : ""}
        onClick={ () => setOpen(!open) }>
        Toggle
      </button>
      <ul className={open ? "show" : ""}>
        <li>
          <a href="/some-link">Some Link</a>
        </li>
        <!-- more menu items -->
      </ul>
    </div>
  )
}

export default Toggle

The useEffect Hook

Next, I want to add some event listeners to the document. This way if someone clicks off the open menu or hits the escape key, the menu will close.

To do that, I create handler functions. The useEffect hook can then return a function to clean up the event handler when it's done. This removes the handler from the document so that it doesn't interfere with any other handlers.

import React, { useState, useEffect } from 'react'

const Toggle = ({ id }) => {
    
  const [ open, setOpen ] = useState(false)
  
  // monitor the state of the toggle
  // add/remove click event handler to the document
  useEffect(() => {
    const clickHandler = ({ target }) => {
      const container = document.getElementById(`container-${id}`);
      if (container.contains(target)) return;
      setOpen(false);
    };

    document.addEventListener("click", clickHandler);

    // these functions clean up the event listeners
    return () => document.removeEventListener("click", clickHandler);
  });

  // same but for keypresses
  // if the esc key is pressed close the toggles
  useEffect(() => {
    const keyHandler = ({ keyCode }) => {
      if (keyCode !== 27) return;
      setOpen(false);
    };
    document.addEventListener("keydown", keyHandler);

    return () => document.removeEventListener("keydown", keyHandler);
  });
  
  // assumes that show and active are classes for css
  return (
    <div id={id}>
      <button 
        aria-expanded={open === true ? "true" : "false"}
        className={open === true ? "active" : ""}
        onClick={ () => setOpen(!open) }>
        Toggle
      </button>
      <ul className={open ? "show" : ""}>
        <li>
          <a href="/some-link">Some Link</a>
        </li>
        <!-- more menu items -->
      </ul>
    </div>
  )
}

export default Toggle

Conclusion

The finished example shows how to abstract the menu itself into a separate component. That way you can create multiple menus by passing it an array of menu objects.

It took a lot less time than I thought to learn how to use these hooks. The next time I need to make a React project I'll definitely use these.