DEV Community

Chandrashekhar Kachawa
Chandrashekhar Kachawa

Posted on • Originally published at ctrix.pro

Mastering Custom Hooks in React: A Developer's Guide

React hooks revolutionized how we write components. But the true power of this system unfolds when you move beyond the built-in hooks and start creating your own. Custom hooks are the most important pattern for sharing stateful logic between components, and mastering them is a key step in becoming a proficient React developer.

This guide covers how to build them, when they are necessary, and—just as importantly—when to avoid them, with advanced tips integrated throughout.

What is a Custom Hook?

A custom hook is simply a JavaScript function whose name starts with use and that calls other hooks. That's it. It's a convention that allows you to extract component logic into a reusable function.

How to Create a Custom Hook

Let's start by identifying a piece of logic we might want to reuse. A common scenario is managing a boolean toggle state.

Before (In a Component):

function MyComponent() {
  const [isOpen, setIsOpen] = useState(false);

  const toggle = () => setIsOpen(!isOpen);

  return (
    <div>
      <button onClick={toggle}>{isOpen ? 'Close' : 'Open'}</button>
      {isOpen && <p>Content is visible!</p>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is fine, but if you need this toggle logic in many components, repeating it is tedious. Let's extract it into a useToggle hook.

After (Creating the useToggle Hook):

import { useState, useCallback } from 'react';

export function useToggle(initialState = false) {
  const [state, setState] = useState(initialState);

  // Senior-level tip: Always wrap functions returned from hooks in `useCallback`.
  // This ensures the function reference is stable and prevents unnecessary
  // re-renders in child components that might receive it as a prop.
  const toggle = useCallback(() => setState(prevState => !prevState), []);

  return [state, toggle];
}
Enter fullscreen mode Exit fullscreen mode

Using the Custom Hook:

import { useToggle } from './useToggle';

function MyComponent() {
  const [isOpen, toggleIsOpen] = useToggle(false);

  return (
    <div>
      <button onClick={toggleIsOpen}>{isOpen ? 'Close' : 'Open'}</button>
      {isOpen && <p>Content is visible!</p>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We've successfully extracted the stateful logic into a clean, reusable, and testable function.

Advanced Strategy: Return an Array or an Object?

  • Return an array (like [value, updateFunction]) when the hook has one primary value and its updater. This mimics useState and allows the consumer to name the returned values freely.
  • Return an object (like { data, isLoading, error }) when the hook returns multiple, distinct values. This is more readable as the consumer knows exactly what they are getting (e.g., const { data } = useFetch(...)).

When to Create a Custom Hook

Knowing when to abstract logic into a hook is a skill.

  1. When Logic is Repeated (DRY Principle): The most obvious sign. If you find yourself copying and pasting the same block of useState and useEffect calls across multiple components, it's time for a hook. A classic example is a useFetch hook for data fetching.

    function useFetch(url) {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState(null);
    
      useEffect(() => {
        // ... fetch logic using the url ...
      }, [url]); // Re-runs when URL changes
    
      return { data, loading, error };
    }
    
  2. To Simplify Complex Components: Sometimes a single component becomes bloated with complex state interactions, timers, or event listeners. Abstracting this logic into one or more custom hooks can make the component itself much cleaner and focused only on rendering the UI.

  3. To Encapsulate Context (The useAuth Pattern): A powerful senior-level pattern is to couple a Context with a custom hook. Instead of forcing consumers to import useContext and AuthContext everywhere, you provide a simple useAuth hook.

    // in auth-context.js
    const AuthContext = createContext(null);
    
    // ... AuthProvider component ...
    
    // The custom hook that consumers will use
    export function useAuth() {
      const context = useContext(AuthContext);
      if (context === null) {
        throw new Error('useAuth must be used within an AuthProvider');
      }
      return context;
    }
    
    // In a component:
    // const { user, login } = useAuth(); // Clean and simple!
    

When NOT to Create a Custom Hook

Abstraction has a cost. Avoid these anti-patterns.

  1. Premature Abstraction: Don't create a hook for something you've only written once. Wait until you need the same logic at least a second time. Creating abstractions too early often leads to overly complex code that is harder to change.

  2. Wrapping a Single, Simple Hook: If your custom hook only contains a single useState call and no other logic, it's a needless layer of indirection. const [value, setValue] = useState() is already as simple as it gets.

  3. For Sharing UI: This is the most important rule. Hooks are for logic, components are for UI. If you want to share JSX, create a component, not a hook. A hook should never return JSX.

Conclusion

Custom hooks are more than just a clever feature; they are a fundamental part of the React design philosophy. They encourage composition over inheritance and allow you to build a library of reusable, stateful logic that is specific to your application. By learning to identify the right moments for abstraction and applying advanced patterns like memoizing callbacks and encapsulating context, you can write React applications that are cleaner, more maintainable, and easier to scale.

Top comments (0)