Using Context and Custom Hooks to Manage a Toggle Menu

My solution to closing a toggle menu at times that it makes sense for the user experience.

I set out to a build a new, beautiful portfolio site after graduating from my bootcamp. I knew that I didn't want to use any template sites to just get it done quickly - I wanted to build something authentic with React. I encountered a few hiccups along the way. One of them was managing whether or not my navigation menu would be open upon certain user-generated events. Here's how I managed to close my menu without the user clicking it directly.

STEP 1: Creating states and passing them through context.

// track clicks that should close the menu, but don't
const [clickEvent, setClickEvent, clickEventRef] = useState(false);

// state to track whether the menu is open or not
const [menuIsOpen, setMenuIsOpen] = useState(false);

// define a context object with the states you need
const context = { 
    clickEvent,
    setClickEvent,
    clickEventRef,
    menuIsOpen,
    setMenuIsOpen,
};

Using the useStateRef package, I make two new states. One to track click events that should close the menu, but are not direct clicks on the toggle button itself. The other simply tracks whether the menu is open or not. The clickEvent state will be used like a toggle flip-flop.

STEP 2: Provide the context to the routes.

// in a file called context.js, create context object with createContext hook.
import { createContext } from "react";
export const MenuContext = createContext(null);

/* import that context object where you will be wrapping your routes 
in the context. here's what mine looks like in that file.
*/

import { MenuContext } from "../util/context.js";

/* now you will wrap the routes in the context provider. make sure
the context definition containing your states is defined in
the same file as your routes
*/ 

const context = { 
    clickEvent,
    setClickEvent,
    clickEventRef,
    menuIsOpen,
    setMenuIsOpen,
};

return (
    <MenuContext.provider value={context}>
        <Header />
        <Routes />
        <Footer />
    </MenuContext.provider>
);

If you've never used context or the createContext hook before, think of the MenuContext.provider tag as a container that gives the components inside access to the value attribute. Since the context and the routes are in the same tag, the routes have access to the context.

Cool! Now we've provided the context (the states) to our entire application. This is usually not ideal, but in this case, it's fine :D

STEP 3: Use the useContext hook to use the state values.

The first place I needed to import these states and affect them is in my header component. You will need to import useContext and the actual context instance made with create context in context.js anywhere that you need to do this.

// your import statements may look different but here's mine
import React, {useEffect, useContext, useRef, useState} from "react";
import { MenuContext } from "../utils/context";

export default function Header() {
    // "tell" react you want the context in the MenuContext.provider
    const context = useContext(MenuContext)
    ...
}

First, since the menuIsOpen state is supposed to track whether the menu is open or not, I put this functionality in.

<Navbar.Toggle
    onClick={() => context.setMenuIsOpen(() => !context.menuIsOpen)}
    ...
/>

Now the state will be representative of the menu... let's try to keep it that way moving on! This turned out to be easier said than done...

STEP 4: Closing the menu upon some other click... but how?

What to do next took me a little bit of time to figure out... I knew that I needed to close the menu without the user clicking the menu button itself for intuition's sake, but how? This is where useRef came in handy.

const toggleHamburger = useRef();

// in the return statement, same toggle from earlier
<Navbar.Toggle ref={toggleHamburger} ... />

At this point, React has a reference to affect this element. Now upon some other event the user generates in which we want the menu to close, we can have React click this button for us! For me, a good reason to close the menu was if the user clicks one of the options in it. Like this:

How do you do this?

You can write a handleClick function. But this might get repetitive, as the goal is to be able to close this menu upon some event across the application. We will want to be able to export/import this functionality in some way. What if we build a custom hook?

// here is the custom hook I built in a file simply named useSideClick.js
export default function useSideClick({
  clickEvent,
  setClickEvent,
  clickEventRef,
  setMenuIsOpen,
}) {
  return function () {
    setClickEvent(() => !clickEvent);
    setMenuIsOpen(() => clickEventRef);
  };
}

clickEventRef makes sure that we have the most current state of clickEvent after a recent change. It is necessary because there is always a possibility that a change in state will take too long to be referenced immediately afterward thanks to state changes being a little bit asynchronous.

STEP 5: Using the custom hook.

When we use the hook, we will have to provide it the props it wants. That should be easy. We already have the context in the routes!

// in Header
import React, { useEffect, useContext, useRef, useState } from "react";
import { MenuContext } from "../utils/context";
import useSideClick from "../util/useSideClick";

export default function Header() {
    const context = useContext(MenuContext);
    const handleSideClick = useSideClick(context);
    ...
}

Alright... now, we've made a custom hook that returns a function that changes the clickEvent and menuIsOpen states. We have grabbed an instance of that function. Now we have to call that function upon clicks on the nav links and have a useEffect that reacts to a change in clickEvent's state.

export default function Header() {

    // handleSideClick changes clickEvent, so the useEffect will catch this change.
    useEffect(handleClickSideEffect, [context.clickEvent]);
    function handleClickSideEffect() {

        // we don't want it to toggle if it is already closed
        if (context.menuIsOpen) {
            // click the toggle button using current toggleHamburger ref we made earlier
            toggleHamburger.current.click();

            /* reflect the change to the menu in its state. we can be explicit
            and set it to false since we know we are closing it.
            */
            context.setMenuIsOpen(() => false);
        }
    }

    return (
        ...
        <Nav.Link onClick={handleSideClick}>Home</Nav.Link>
        ...
    );
}

Voila. Now, our toggle menu closes upon one of its nav links being clicked. Awesome!

Of course, since we made this a custom hook, we can import it and use it anywhere that has the same context. Another time I use it in my portfolio site is if either button on the contact page is clicked. Essentially, I want the menu to close any time the route changes. Both of these buttons change the route.

You are more than welome to view my source code for my portfolio site if you need more context ;) ... about what is going on here in these code snippets!

You can always contact me via LinkedIn or at [email protected] as well.

15