Tutorials / React

React Custom Hooks - Complete Deep Dive Guide

Mahesh Mahesh Waghmare
4 min read Intermediate

Custom hooks are one of React’s most powerful features for code reuse and logic sharing. This comprehensive guide covers everything from basic custom hooks to advanced patterns and best practices.

Introduction to Custom Hooks

Custom hooks let you extract component logic into reusable functions. They’re regular JavaScript functions that can use other hooks.

Benefits:

  • Code reusability
  • Logic separation
  • Easier testing
  • Better organization
  • Shared stateful logic

Naming Convention:

  • Must start with “use”
  • Examples: useCounter, useFetch, useLocalStorage

Rules of Hooks

Essential Rules:

  • Only call hooks at the top level
  • Don't call hooks inside loops, conditions, or nested functions
  • Only call hooks from React function components or custom hooks
  • Custom hooks must start with "use"

Example - Correct:

function MyComponent() {
    const [count, setCount] = useState(0);
    const data = useFetch('/api/data');
    return <div>{count}</div>;
}

Example - Incorrect:

function MyComponent() {
    if (condition) {
        const [count, setCount] = useState(0); // Wrong!
    }
    return <div>{count}</div>;
}

Creating Custom Hooks

Basic Custom Hook

useCounter Hook:

import { useState } from 'react';

function useCounter(initialValue = 0) {
    const [count, setCount] = useState(initialValue);
    
    const increment = () => setCount(c => c + 1);
    const decrement = () => setCount(c => c - 1);
    const reset = () => setCount(initialValue);
    
    return { count, increment, decrement, reset };
}

// Usage
function Counter() {
    const { count, increment, decrement, reset } = useCounter(0);
    
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
            <button onClick={reset}>Reset</button>
        </div>
    );
}

Hook with Side Effects

useFetch Hook:

import { useState, useEffect } from 'react';

function useFetch(url) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
        setLoading(true);
        fetch(url)
            .then(res => res.json())
            .then(data => {
                setData(data);
                setError(null);
            })
            .catch(err => setError(err))
            .finally(() => setLoading(false));
    }, [url]);
    
    return { data, loading, error };
}

// Usage
function DataComponent() {
    const { data, loading, error } = useFetch('/api/data');
    
    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    return <div>{JSON.stringify(data)}</div>;
}
Advertisement

Common Hook Patterns

useLocalStorage Hook

function useLocalStorage(key, initialValue) {
    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = window.localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch (error) {
            return initialValue;
        }
    });
    
    const setValue = (value) => {
        try {
            setStoredValue(value);
            window.localStorage.setItem(key, JSON.stringify(value));
        } catch (error) {
            console.error(error);
        }
    };
    
    return [storedValue, setValue];
}

useDebounce Hook

import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
    const [debouncedValue, setDebouncedValue] = useState(value);
    
    useEffect(() => {
        const handler = setTimeout(() => {
            setDebouncedValue(value);
        }, delay);
        
        return () => clearTimeout(handler);
    }, [value, delay]);
    
    return debouncedValue;
}

useToggle Hook

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

Advanced Hook Patterns

useReducer Hook Pattern

function useAsync(asyncFunction) {
    const [state, dispatch] = useReducer(asyncReducer, {
        data: null,
        loading: false,
        error: null,
    });
    
    const execute = useCallback(async (...args) => {
        dispatch({ type: 'START' });
        try {
            const data = await asyncFunction(...args);
            dispatch({ type: 'SUCCESS', payload: data });
        } catch (error) {
            dispatch({ type: 'ERROR', payload: error });
        }
    }, [asyncFunction]);
    
    return { ...state, execute };
}

Composing Hooks

function useUserData(userId) {
    const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
    const [preferences, setPreferences] = useLocalStorage(`user-${userId}-prefs`, {});
    
    return {
        user,
        preferences,
        loading,
        error,
        updatePreferences: setPreferences,
    };
}
Advertisement

Testing Custom Hooks

Using React Testing Library

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

test('increments count', () => {
    const { result } = renderHook(() => useCounter(0));
    
    act(() => {
        result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
});

Conclusion

Custom hooks enable:

  • Reusable component logic
  • Better code organization
  • Easier testing
  • Shared stateful logic

Key principles:

  • Start with “use”
  • Follow hooks rules
  • Return values consistently
  • Handle edge cases
  • Document hook behavior

Mastering custom hooks significantly improves React development efficiency and code quality.

Advertisement
Mahesh Waghmare

Written by Mahesh Waghmare

I bridge the gap between WordPress architecture and modern React frontends. Currently building tools for the AI era.

Follow on Twitter