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;
}
🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน 🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน

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;
};
🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน 🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน

and on the screens that have a side menu:

const SomeScreen = () => {
  useSideMenu()

  return ...yada...yada...yada
}
🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน 🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน

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>
🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน 🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน

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 />
}

🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน 🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน

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;
};
🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน 🏆 ยอดนิยม: wolverineks/react hook usesidemenu e - ประเด็นร้อน

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)