Home
> Building a Custom Menu Hook with Qwik
Get updates on future FREE course and blog posts!
Subscribe

Building a Custom Menu Hook with Qwik

10 min read

Jonathan Gamble

jdgamble555 on Sunday, February 26, 2023 (last modified on Friday, March 8, 2024)

After using Svelte for a while, I wanted to write a custom store-like hook in Qwik, which allows me to deal with menus. It also needs to be reusable, and you may have more than one.

Qwik, like React, wants to share components using Context. I also wanted to return a few functions that a menu may need like: open, close, toggle, and isOpen. Also, like React, you can just return the functions you want to use in your custom hook. However, one step at a time.

useClickOutside#

The first thing I needed to tackle, was dealing with closing the menu when a person clicks outside of the menu. This is one of those normal things we don't realize we need to write sometimes. There are patterns for this in JS and other Frameworks, so I needed to translate them.

I first came up with this:

	useVisibleTask$(() => {
    const onClick = (event: Event) => {
        if (!ref.value) { return; }
        const target = event.target as HTMLElement;
        if (!ref.value.contains(target)) {
            onClickOut();
        }
    };
    document.addEventListener("click", onClick);
    return () => document.removeEventListener("click", onClick);
});

This works fine. Here useVisibleTask$ is similar to React's useEffect. However, Qwik has built in functions to simplify this even more with useOnDocument().

Here is the final useClickOutside hook code:

useClickOutside.ts#

	import type { QRL, Signal } from '@builder.io/qwik';
import { $, useOnDocument } from '@builder.io/qwik';

export const useClickOutside = (
    ref: Signal<HTMLElement | undefined>,
    onClickOut: QRL<() => void>
) => {

    useOnDocument("click", $((event) => {
        if (!ref.value) { return; }
        const target = event.target as HTMLElement;
        if (!ref.value.contains(target)) {
            onClickOut();
        }
    }));
};

So, we just need to pass in the element which we consider inside attached to a Signal, and a function to deal with the event.

Context#

In order to deal with no prop drilling, we needed to create a Context. However, I created a custom hook called useShared so that you don't have to think about it:

useShared.ts#

	import {
    createContextId,
    useContext,
    useContextProvider
} from "@builder.io/qwik";

export const sharedContext = <T>(name: string) =>
    createContextId<T>('io.builder.qwik.' + name);

export const getShared = <T extends object>(name: string) =>
    useContext<T, null>(sharedContext(name), null);

export const createShared = <T extends object>(name: string, content: T) =>
    useContextProvider<T>(sharedContext(name), content);

export const useShared = <T extends object>(
    hook: () => T,
    name: string
) => {

    // get context if exists
    const shared = getShared<T>(name);
    if (shared) {
        return shared;
    }

    // return new shared context
    const _shared = hook();
    createShared(name, _shared);
    return _shared;
};

This will take care of the menu hook and any other shared hook you want to use, automatically.

Core Code#

The core code here is pretty simple:

	const menu = useSignal<boolean>(false);
const menuRef = useSignal<HTMLElement>();

useClickOutside(menuRef, $(() => {
    menu.value = false;
}));

const menuObj = {
    close: $(() => { menu.value = false }),
    open: $(() => { menu.value = true }),
    toggle: $(() => { menu.value = !menu.value }),
    isOpen: menu.value,
    ref: menuRef
};

Basically you will create two signals, one for the menu reference, and the other for open or closed. Then you return the functions to do what you need. Our useClickOutside with close the menu as desired. Notice in Qwik, you need QRL References with $(), instead of functions, in order to allow the resumability part to work correctly.

Final Code#

To polish this, I wanted to make the hook reusable in children and parents. So, I created a custom hook with contexts:

useMenu.ts#

	import { $, useSignal } from '@builder.io/qwik';
import { useClickOutside } from './useClickOutside';
import { useShared } from './useShared';

const _useMenu = () => {

    const menu = useSignal<boolean>(false);
    const menuRef = useSignal<HTMLElement>();

    useClickOutside(menuRef, $(() => {
        menu.value = false;
    }));

    const menuObj = {
        close: $(() => { menu.value = false }),
        open: $(() => { menu.value = true }),
        toggle: $(() => { menu.value = !menu.value }),
        isOpen: menu.value,
        ref: menuRef
    };

    return menuObj;
};

export const useMobileMenu = () => useShared(_useMenu, 'mobile-menu');
export const useMainMenu = () => useShared(_useMenu, 'main-menu');

Usage#

I may have a layout component with something like this:

	export default component$(() => {

  const mobileMenu = useMobileMenu();
  ...

Here, mobile-menu is the key for this particular menu. You can export as many custom menu hooks as you want, just be sure to change the key.

The menu button itself, may be this:

	<button type="button" onClick$={() => mobileMenu.toggle()}>....</button>

You may want to toggle the menu instead of just open, in case it is already open.

And the menu itself with items:

	{mobileMenu.isOpen && <Menu />}

Child#

Your child Menu component, could look like this:

	export default component$(() => {

  const menu = useMobileMenu();
  return (
    <div ref={menu.ref}>
    ... menu html here
    </div>
  );
};

Now notice you can click outside the menu, and it closes. If you click on the button itself again, it closes. And of course, if you click on the close menu item, it closes.

And...#

So this is most likely over engineering. However, I like to follow the single responsibility principle and use custom hooks for my functionality. It is also beautiful coding to me.

You could follow these same patterns for any hook, and I believe you should. This will make your code more readable, easier to update, and more reusable. Simply use and import the useShared hook.

posts/c629edf2-a9e3-450c-983e-ab57bdec9d72/mgknskgcdlcwromtfb6ebei4m.jpg

Repo: GitHub

Live Example: StackBlitz

Qwik is certainly very easy compared to React.

J


Related Posts

© 2024 Code.Build