Skip to main content

Custom React Hooks: Advanced useToggle with useReducer

An advanced custom hook combines useReducer with useCallback to manage complex state logic in a reusable function. The useToggle hook can be enhanced from a simple toggle to a fully-featured state manager that not only toggles a boolean but also explicitly sets it to true or false. Using useReducer instead of useState makes your custom hooks more flexible and scalable, while useCallback ensures memoized functions that prevent unnecessary re-renders in child components.

📖 Introduction

Following our creation of a simple useToggle hook, this article explores how we can make it even more powerful. We will add features to our useToggle hook that allow us to set the value to true or false directly, in addition to toggling it.


📚 Prerequisites

Before we begin, please ensure you have a solid grasp of the following concepts:

  • All concepts from Part 1 of this series.
  • The useReducer hook.

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • The useReducer Hook: Why useReducer is a good choice for managing state with multiple actions.
  • Core Implementation: How to refactor the useToggle hook to use useReducer.
  • Practical Application: How to use the enhanced useToggle hook in a component.

🧠 Section 1: The Core Concepts of useReducer for useToggle

Our current useToggle hook is great, but it only allows us to toggle the value. What if we want to explicitly set the value to true or false? We could add more functions to our hook, but a cleaner approach is to use the useReducer hook.

useReducer is an alternative to useState. It is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

For our useToggle hook, we can use a reducer to handle three actions: TOGGLE, SET_TRUE, and SET_FALSE.


💻 Section 2: Deep Dive - Implementation and Walkthrough

Let's refactor our useToggle hook to use useReducer.

2.1 - The useToggle Hook with useReducer

// useToggle.js
import { useReducer, useCallback } from 'react';

const toggleReducer = (state, action) => {
switch (action.type) {
case 'TOGGLE':
return !state;
case 'SET_TRUE':
return true;
case 'SET_FALSE':
return false;
default:
return state;
}
};

function useToggle(initialValue = false) {
const [value, dispatch] = useReducer(toggleReducer, initialValue);

const toggle = useCallback(() => dispatch({ type: 'TOGGLE' }), []);
const setTrue = useCallback(() => dispatch({ type: 'SET_TRUE' }), []);
const setFalse = useCallback(() => dispatch({ type: 'SET_FALSE' }), []);

return [value, toggle, setTrue, setFalse];
}

export default useToggle;

Step-by-Step Code Breakdown:

  1. toggleReducer: We define a reducer function that takes the current state and an action, and returns the new state.
  2. useReducer(toggleReducer, initialValue): We use the useReducer hook with our reducer and an initial value. It returns the current state (value) and a dispatch function.
  3. dispatch({ type: '...' }): We create memoized functions (toggle, setTrue, setFalse) that call the dispatch function with the appropriate action type.
  4. return [value, toggle, setTrue, setFalse];: We return the current value and our new functions.

🛠️ Section 3: Project-Based Example: A Modal Component

Now, let's use our enhanced useToggle hook to manage a modal component.

// ModalComponent.js
import React from 'react';
import useToggle from './useToggle';

function ModalComponent() {
const [isOpen, toggle, openModal, closeModal] = useToggle(false);

return (
<div>
<button onClick={openModal}>Open Modal</button>
{isOpen && (
<div className="modal">
<div className="modal-content">
<p>This is a modal!</p>
<button onClick={closeModal}>Close</button>
</div>
</div>
)}
</div>
);
}

export default ModalComponent;

Code Breakdown:

  1. const [isOpen, toggle, openModal, closeModal] = useToggle(false);: We destructure the values from our useToggle hook. We can rename setTrue to openModal and setFalse to closeModal for better readability.
  2. <button onClick={openModal}>: We use the openModal function to open the modal.
  3. <button onClick={closeModal}>: We use the closeModal function to close the modal.

Key Takeaways

  • useReducer is Powerful for Complex State: When you have multiple related state updates or actions, useReducer provides a cleaner and more maintainable approach than multiple useState calls.
  • Memoize Callback Functions: Using useCallback on the functions returned from your custom hook prevents unnecessary re-renders in child components that receive these functions as props.
  • Custom Hooks Promote Reusability: By extracting state logic into a custom hook, you can reuse the same pattern across multiple components.
  • Naming Functions for Clarity: Renaming returned functions (like setTrue to openModal) makes your component code more readable and self-documenting.
  • Composition Over Complexity: You can combine multiple hooks (useReducer and useCallback) within a custom hook to create powerful, reusable state management logic.

Frequently Asked Questions

When should I use useReducer instead of useState?

Use useReducer when your state has multiple related values that change together, when the next state depends on the previous state, or when you have multiple actions that modify state in complex ways. For simple, single-value state, useState is usually sufficient.

Why do I need useCallback in my custom hooks?

useCallback memoizes the functions you return from your hook. This prevents child components that receive these functions as props from re-rendering unnecessarily on every parent render, improving performance.

Can I use multiple custom hooks in one component?

Yes, absolutely. You can use multiple custom hooks in a single component, and you can even compose custom hooks on top of other custom hooks. This is one of the powerful features of hooks.

How do I test custom hooks?

You can test custom hooks using the React Testing Library's renderHook utility. This allows you to test the hook in isolation without needing to wrap it in a component. Alternatively, you can write component tests that use the custom hook.

What if I need to add more actions to the toggle reducer?

Simply add more case statements to your toggleReducer function and create corresponding useCallback memoized functions for each new action. Return all the functions from your hook so consumers can access them.


Further Reading


Glossary

  • useReducer: A React hook that is an alternative to useState for managing state with complex logic.
  • Reducer: A pure function that takes the current state and an action, and returns the new state.
  • Dispatch: A function that sends an action to a reducer.
  • Action: An object describing what should happen to the state, typically with a type property.
  • useCallback: A React hook that memoizes a function, returning the same function reference unless its dependencies change.

➡️ Next Steps

You now have a solid understanding of how to create a more advanced custom hook. In the next article, "A Custom Hook for Data Fetching: useFetch (Part 1)", we will build a reusable hook for fetching data from an API.

Thank you for your dedication. Stay curious, and happy coding!