Using React Custom Hooks

Scenario

I'm tasked with handling the logout logic for our React application. We have a redux store that stores a user's state once we pass a JWT token upon login. Simple right?

What are hooks and and custom hooks?

Hooks

In React, a hook is a special kind of function that allows you to use state and other React features in functional components. Hooks were introduced in React 16.8 to provide a way to use state and life-cycle features in functional components, which were previously only available in class components.

Custom hooks

Custom hooks are simply JavaScript functions that are used to encapsulate and reuse logic.

Example Problem

Let's take a look at one of our components. This component should render a button for logout but as it is now, it can't do that. We have several components like this but for now, let's assume, there is only one of them.

const LandlordNavLinks = ({ setClickedLink }) => {

  return (
    <VStack align={'start'} gap={'0.75rem'}>
      {getNavRoutes(routes, setClickedLink)}
      <Button
        variant={'ghost'}
        leftIcon={<Image src={navMenuIcons[7]} />}
        fontSize={'14px'}
      >
        Logout
      </Button>
    </VStack>
  );
};

export default LandlordNavLinks;

If we were to include the logout logic, our code would look like this;

const LandlordNavLinks = ({ setClickedLink }) => {
  const navigate = useNavigate();
  const logoutHandler = () => {
    store.dispatch(logout({}));
    navigate('/');
  };

  return (
    <VStack align={'start'} gap={'0.75rem'}>
      {getNavRoutes(routes, setClickedLink)}
      <Button
        variant={'ghost'}
        leftIcon={<Image src={navMenuIcons[7]} />}
        fontSize={'14px'}
        onClick={logoutHandler}
      >
        Logout
      </Button>
    </VStack>
  );
};

export default LandlordNavLinks;

This is satisfactory as it is only being used in one function. But there are instances (like we will see in our case), where a function is to be used in more than one component. The novice thing to do would be to copy-paste the logic across the different components. Writing a custom hook is a much better solution.

Why we should avoid copy/paste

Avoiding duplication of code is crucial for a number of reasons:

  1. In the event of a bug occurring within the hook, we will only fix it in one place

  2. it saves on the time one is to read and maintain code.

  3. for web applications, it helps to reduce the bundle size

Solution

As stated before, custom hooks help encapsulate logic that can be reused. In our case, we want the logic we're interested in is the function logoutHandler. This function does a number of things:

  1. it uses the useDispatch hook from react-redux for dispatching actions. The action in this case being logout as it deletes our token from the application. The primary logic for logout is handled in our logic function that we call inside dispatch.

  2. it uses the useNavigate hook from react-router to get the navigate function for programmatic navigation. As such, the user is navigated to the home page upon logout.

If we have another component that requires the use of the logoutHandler, it would be wise to use a custom hook to solve it:

const MobileLandlordNavLinks = ({ onClose }) => {
  const logoutHandler = useLogout();
  return landlordMobileNavMenus.map((menu, index) => (
    <>
      {menu === 'Logout' ? (
        <Button
          variant={'ghost'}
          leftIcon={<Image src={navMenuIcons[7]} h='16px' />}
          fontSize={'14px'}
        >
          Logout
        </Button>
      ) : (
      {/* the rest of the code */}
      )}
    </>
  ));
};

export default MobileLandlordNavLinks;

Our custom hook

Our hook is supposed to serve the two purposes as our original logoutHandler. It would look like this:

const useLogout = () => {
  const navigate = useNavigate();
  const logoutHandler = () => {
    store.dispatch(logout({}));
    navigate('/');
  };

  return logoutHandler;
};

export default useLogout;

As you can see, the hook is able to dispatch the logout action and navigate the user to the home page. This is made possible by defining the logoutHandler inside the useLogout hook.

Now that we have transferred our logic to a hook, we can refactor our components to use it accordingly.

First component:

const LandlordNavLinks = ({ setClickedLink }) => {
  const logoutHandler = useLogout();

  return (
    <VStack align={'start'} gap={'0.75rem'}>
      {getNavRoutes(routes, setClickedLink)}
      <Button
        variant={'ghost'}
        leftIcon={<Image src={navMenuIcons[7]} />}
        fontSize={'14px'}
        onClick={logoutHandler}
      >
        Logout
      </Button>
    </VStack>
  );
};

export default LandlordNavLinks;

Second component:

const MobileLandlordNavLinks = ({ onClose }) => {
  const logoutHandler = useLogout();
  return landlordMobileNavMenus.map((menu, index) => (
  const logoutHandler = useLogout();

    <>
      {menu === 'Logout' ? (
        <Button
          variant={'ghost'}
          leftIcon={<Image src={navMenuIcons[7]} h='16px' />}
          fontSize={'14px'}
          onClick={logoutHandler}

        >
          Logout
        </Button>
      ) : (
      {/* the rest of the code */}
      )}
    </>
  ));
};

export default MobileLandlordNavLinks;

We can use this useLogout hook in as many components as we want. Essentially, we've made our code cleaner, and more modular, following the principles of reusable and encapsulated logic that React hooks encourage.