DEV Community

Kevin Sullivan
Kevin Sullivan

Posted on • Edited on

React Hook: useSideMenu()

TLDR;

Here is a snack similar to how I'm implementing a side menu in a React Native / Expo app
https://snack.expo.io/@wolverineks/withsidemenu

Background

I'm currently building a React Native / Expo app for a client. The app uses React Router, and React Native Drawer.

Some of the routes have a side menu, and some do not. So I wrote context like...

interface SideMenuContext {
  open: () => void;
  close: () => void;
  enable: () => void;
  disable: () => void;
  enabled: boolean;
  shouldOpen: boolean;
}
Enter fullscreen mode Exit fullscreen mode

and a hook like...

export const useSideMenu = () => {
  const sideMenu = React.useContext(SideMenuContext);
  if (sideMenu === undefined) {
    throw new Error("useSideMenu must used in a SideMenuProvider");
  };
  const { enable, disable, close } = sideMenu

  React.useEffect(() => {
    enable();

    return () => {
      disable();
      close();
    };
  }, []);

  return sideMenu;
};
Enter fullscreen mode Exit fullscreen mode

and on the screens that have a side menu:

const SomeScreen = () => {
  useSideMenu()

  return ...yada...yada...yada
}
Enter fullscreen mode Exit fullscreen mode

Can anyone spot the undesirable behavior?

So, I noticed a few things about this approach that I didn't like.

  1. The imperative nature of the hook api meant that if (for some reason) multiple components that useSideMenu are mounted simultaneously, removing any of them would disable the side menu. The behavior I was looking for was, disabling the side menu only if all of the components were unmounted.

  2. When testing the screens in isolation, a <SideMenuProvider /> would have to be mounted, or the hook would throw an error.

Next Steps

To overcome the second issue, I've written a component, <WithSideMenu />, and moved the useSideMenu() call from just inside the screens, to just outside of the screens...

<WithSideMenu> 
  <SomeComponent />
</WithSideMenu>
Enter fullscreen mode Exit fullscreen mode

And, to overcome the first issue, I've rewritten the context to...

interface SideMenuContext {
  open: () => void;
  close: () => void;
  register: () => () => void; // <- returns an "unregister"
  enabled: boolean;
  shouldOpen: boolean;
<Drawer />
}

Enter fullscreen mode Exit fullscreen mode

to be used like...

const WithSideMenu: React.FC = ({ children ) => {
  const sideMenu = useSideMenu()
  const { register } = sideMenu;

  React.useEffect(register, []);

  return typeof children === "function"
    ? children(sideMenu)
    : children;
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

  1. Declarative for the win.
  2. Composition for the win.
  3. Probably some other stuff...

Here is the snack again:

https://snack.expo.io/@wolverineks/withsidemenu

Top comments (0)