DEV Community

Cover image for Using Context and Custom Hooks to Manage a Toggle Menu
Austin J. (github.com/jufudev)
Austin J. (github.com/jufudev)

Posted on • Updated on

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,
};
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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)
    ...
}
Enter fullscreen mode Exit fullscreen mode

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((prev) => !prev)}
    ...
/>
Enter fullscreen mode Exit fullscreen mode

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} ... />
Enter fullscreen mode Exit fullscreen mode

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:

Nav Response

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 useClickSideEffect.js
export const useClickSideEffect = ({
  clickEvent,
  setClickEvent,
  clickEventRef,
  setMenuIsOpen,
}) =>
  () => {
    setClickEvent(() => !clickEvent);
    setMenuIsOpen(() => clickEventRef);
  };
}
Enter fullscreen mode Exit fullscreen mode

clickEventRef makes sure that we have the most current state of clickEvent after a recent change. I believe it is necessary to develop with useStateRef or something like it when you encounter a situation where you must use a state for some other programmatic task immediately after an update.

Side note: State updates are not asynchronous. However, the instantaneous, infinitesimal, yet briefly large, immediately decaying delta in client-side load caused by a newly queued re-render causes processes to slow briefly; enough to cause a race-condition between line 15 that updates the state and line 16 that needs it to be current... yeah, the difference is that small. But big enough. React re-renders cause the DOM to briefly forget, reread, and rebuild itself with JavaScript's instructions... So, be careful describing any state updates as asynchronous. Even though they can appear to be, they are not. This problem heavily depends upon the size of your application, the version of React you are running, and the client-host. Nonetheless, I say useStateRef and others like it are so easy to plug in that there is no point in taking this chance.

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 { useClickSideEffect } from "../util/useClickSideEffect";

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

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>
        ...
    );
}
Enter fullscreen mode Exit fullscreen mode

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 welcome to view my source code if you need more context ;) ... about what is going on here in these code snippets!

You can always contact me via LinkedIn or at jufudev@proton.me as well.

Top comments (0)