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:
In the event of a bug occurring within the hook, we will only fix it in one place
it saves on the time one is to read and maintain code.
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:
it uses the
useDispatch
hook fromreact-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 ourlogic
function that we call insidedispatch
.it uses the
useNavigate
hook fromreact-router
to get thenavigate
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.