BeginnerReact Concepts

Custom Hooks in React – Code Reuse Without the Drama

Custom hooks let you extract and reuse logic across components — cleanly separating behavior and keeping your code DRY

By Rudraksh Laddha

Last month, I was reviewing a pull request with 400+ lines of duplicated fetch logic across components. Sound familiar?

useEffect(() => { /* fetch users */ }, []);
useEffect(() => { /* fetch posts */ }, []);
useEffect(() => { /* fetch comments */ }, []);

This is where custom hooks saved my sanity—and probably prevented a few late-night debugging sessions.

React's custom hooks aren't just about code reuse—they're about creating a consistent, maintainable architecture that scales with your team.

🎯 What Is a Custom Hook?

A custom hook is a JavaScript function that starts with use and encapsulates stateful logic that can be shared between components. Think of it as your component's API for complex behavior.

After building dozens of React apps, I've learned that custom hooks are where the real power lies. They let you extract component logic into reusable functions while maintaining React's rules of hooks.

🤯 Why Custom Hooks Matter in Production

  • Eliminate duplicate logic across components (I've seen 30% reduction in codebase size)
  • Create testable, isolated business logic separate from UI concerns
  • Establish consistent patterns that new team members can follow
  • Enable performance optimizations at the hook level that benefit all consumers

🛠️ Anatomy of a Production-Ready Custom Hook

A well-crafted custom hook should:

  • Start with use (React's convention for hook identification)
  • Follow the Rules of Hooks (no conditional calls, top-level only)
  • Return a consistent interface (object destructuring works well)
  • Handle edge cases and error states
  • Include proper cleanup to prevent memory leaks

🧪 Example: useToggle Hook

// useToggle.js
import { useState, useCallback } from "react";

const useToggle = (initialValue = false) => {
  const [value, setValue] = useState(initialValue);
  
  const toggle = useCallback(() => setValue(prev => !prev), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);
  
  return { value, toggle, setTrue, setFalse };
};

export default useToggle;

Now use it in your components:

import useToggle from "./hooks/useToggle";

const Modal = () => {
  const { value: isOpen, toggle, setFalse: close } = useToggle();

  return (
    <>
      <button onClick={toggle}>Open Modal</button>
      {isOpen && (
        <div className="modal">
          <button onClick={close}>Close</button>
          <p>Modal content here</p>
        </div>
      )}
    </>
  );
};
  • ✅ Consistent API across all toggle implementations
  • ✅ Memoized callbacks prevent unnecessary re-renders
  • ✅ Flexible naming through destructuring

📦 Production Pattern: useFetch with Error Handling

// useFetch.js
import { useState, useEffect } from "react";

const useFetch = (url, options = {}) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const abortController = new AbortController();
    
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(url, {
          ...options,
          signal: abortController.signal
        });
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const result = await response.json();
        setData(result);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();
    
    return () => abortController.abort();
  }, [url, JSON.stringify(options)]);

  return { data, loading, error };
};

export default useFetch;

Usage with proper error boundaries:

const UserProfile = ({ userId }) => {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <NotFound />;

  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
};

🔥 This pattern handles request cancellation, proper error states, and prevents memory leaks—essentials for production apps.

⚡ Why Custom Hooks Beat Alternatives

Benefit Impact on Production Code
Logic reuse Reduces codebase by 20-40% in complex apps
Testability Unit test logic independently from UI
Performance Centralized optimizations benefit all consumers
Team consistency Shared patterns reduce onboarding time
Error handling Consistent error states across the app

🧠 Naming Conventions That Matter

The use prefix isn't just convention—it's how React's linter identifies hooks for rule enforcement.

  • useLocalStorage, useDebounce, useWindowSize
  • localStorageHook, getDebounced, windowSizeUtil

I've seen teams struggle with ESLint errors because they skipped this naming rule. Don't be that team.

⚠️ Production Gotchas I've Learned

Common Mistake How to Avoid It
Conditional hook calls Always call hooks at the top level, use conditions inside
Missing cleanup Return cleanup functions from useEffect
Stale closures Use useCallback and proper dependencies
Over-optimization Don't memo everything—measure first

✅ Custom Hooks: Architecture Decision Guide

When to Create Best Practice
Repeated stateful logic Extract after second duplication
Complex component logic Split when component exceeds 200 lines
API integrations Always wrap in custom hooks for consistency
Browser APIs Abstract for better testing and SSR support
Form handling Create reusable validation and submission logic

FAQs from Real Projects

Should custom hooks return objects or arrays?
I prefer objects for hooks with multiple values (better naming), arrays for simple pairs like useState does. Consider your API's usability.

Can custom hooks call other custom hooks?
Absolutely. Some of my best hooks are compositions of smaller ones. Just follow the Rules of Hooks.

How do I test custom hooks?
Use React Testing Library's renderHook utility. Test the logic, not the implementation details.

Should I always extract logic into hooks?
No. If it's only used once and simple, keep it in the component. Extract when you see patterns or complexity.

Can custom hooks access context?
Yes! Custom hooks can use useContext, making them perfect for abstracting context consumption logic.

❤️ At Learn Virendana, we love creating high-quality React tutorials that simplify complex concepts and deliver a practical, real-world React learning experience for developers