The list of the top 10 custom hooks, with code examples

npmixnpmix
38
React Hooks

Top 10 Custom Hooks for Every React Developer

React custom hooks allow you to extract and reuse logic across components, making your code more maintainable, modular, and readable. In this guide, we cover 10 powerful custom hooks that every React developer should have in their toolbox — complete with examples and common use cases.


1. useFetch

Fetch and manage data effortlessly with built-in support for loading and error states.

javascript
// hooks/useFetch.js
import { useState, useEffect } from 'react';

function useFetch(url) {
  // States to store data, loading status, and errors
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Async function to fetch data
    const fetchData = async () => {
      try {
        const res = await fetch(url);
        const json = await res.json();
        setData(json); // Store data if request succeeds
      } catch (err) {
        setError(err); // Capture error if request fails
      } finally {
        setLoading(false); // Mark loading as complete in all cases
      }
    };
    fetchData();
  }, [url]); // Re-trigger if URL changes

  return { data, loading, error };
}

export default useFetch;

Usage Example:

jsx
// components/UserProfile.jsx
import useFetch from '../hooks/useFetch';

function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
    </div>
  );
}

2. useLocalStorage

Persist state in localStorage for browser-based storage. Perfect for saving user preferences or application state between sessions.

javascript
// hooks/useLocalStorage.js
import { useState } from 'react';

function useLocalStorage(key, initialValue) {
  // Initialize state with stored value or initial value
  const [storedValue, setStoredValue] = useState(() => {
    if (typeof window === 'undefined') {
      return initialValue; // Return initial value on server-side
    }
    
    try {
      // Get value from localStorage
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });
  
  // Function to update value in state and localStorage
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      
      // Save to localStorage (client-side only)
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      console.error(error);
    }
  };
  
  return [storedValue, setValue];
}

export default useLocalStorage;

3. useToggle

Quickly toggle between true and false — great for modals, dropdowns, or dark mode.

javascript
// hooks/useToggle.js
import { useState } from 'react';

function useToggle(initialValue = false) {
  // State to track current value
  const [state, setState] = useState(initialValue);
  // Function to toggle state
  const toggle = () => setState(prev => !prev);
  return [state, toggle];
}

export default useToggle;

Usage Example:

jsx
// Simple example for a dropdown menu
const [isOpen, toggle] = useToggle();

return (
  <>
    <button onClick={toggle}>
      {isOpen ? 'Close' : 'Open'} Menu
    </button>
    {isOpen && <div className="dropdown-menu">Menu Content</div>}
  </>
);

4. useDebounce

Delay updates to prevent unnecessary calls, perfect for search inputs and auto-saving.

javascript
// hooks/useDebounce.js
import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
  // State to store the delayed value
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // Set a delay before updating the value
    const handler = setTimeout(() => setDebouncedValue(value), delay);
    // Clean up timeout if value changes before delay
    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

export default useDebounce;

5. useInterval

Execute a callback repeatedly at specified intervals — cleanly and React-friendly.

javascript
// hooks/useInterval.js
import { useRef, useEffect } from 'react';

function useInterval(callback, delay) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay === null) return;
    const id = setInterval(() => savedCallback.current(), delay);
    return () => clearInterval(id);
  }, [delay]);
}

export default useInterval;

6. useWindowSize

Track the current window size for responsive rendering.

javascript
// hooks/useWindowSize.js
import { useState, useEffect } from 'react';

function useWindowSize() {
  const [size, setSize] = useState({
    width: typeof window !== 'undefined' ? window.innerWidth : 0,
    height: typeof window !== 'undefined' ? window.innerHeight : 0,
  });

  useEffect(() => {
    const handleResize = () => setSize({
      width: window.innerWidth,
      height: window.innerHeight,
    });
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

export default useWindowSize;

7. useWebSocket

Add real-time capabilities with automatic reconnection and message handling.

javascript
// hooks/useWebSocket.js
import { useState, useEffect, useRef, useCallback } from 'react';

function useWebSocket(url, options = {}) {
  const [data, setData] = useState(null);
  const [status, setStatus] = useState('CONNECTING');
  const [error, setError] = useState(null);
  const [reconnectAttempt, setReconnectAttempt] = useState(0);
  
  const ws = useRef(null);
  const reconnectTimeoutRef = useRef(null);
  
  const { 
    reconnectInterval = 2000,
    maxReconnectAttempts = 5,
    onOpen,
    onClose,
    onMessage,
    onError,
    protocols
  } = options;

  // Function to send a message
  const sendMessage = useCallback((message) => {
    if (ws.current && ws.current.readyState === WebSocket.OPEN) {
      ws.current.send(typeof message === 'string' ? message : JSON.stringify(message));
      return true;
    }
    return false;
  }, []);

  // Function to connect
  const connect = useCallback(() => {
    // Clear any existing connection
    if (ws.current) {
      ws.current.close();
    }

    // Create a new WebSocket connection
    try {
      ws.current = new WebSocket(url, protocols);
      setStatus('CONNECTING');
      
      // Connection opened
      ws.current.onopen = (event) => {
        setStatus('OPEN');
        setReconnectAttempt(0);
        if (onOpen) onOpen(event);
      };
      
      // Listen for messages
      ws.current.onmessage = (event) => {
        try {
          const parsedData = JSON.parse(event.data);
          setData(parsedData);
          if (onMessage) onMessage(parsedData, event);
        } catch (e) {
          // Handle non-JSON messages
          setData(event.data);
          if (onMessage) onMessage(event.data, event);
        }
      };
      
      // Connection closed
      ws.current.onclose = (event) => {
        setStatus('CLOSED');
        if (onClose) onClose(event);
        
        // Try to reconnect
        if (reconnectAttempt < maxReconnectAttempts) {
          reconnectTimeoutRef.current = setTimeout(() => {
            setReconnectAttempt(prev => prev + 1);
            connect();
          }, reconnectInterval);
        } else {
          setError(new Error('Maximum reconnect attempts reached'));
        }
      };
      
      // Connection error
      ws.current.onerror = (event) => {
        setStatus('ERROR');
        setError(event);
        if (onError) onError(event);
      };
    } catch (error) {
      setStatus('ERROR');
      setError(error);
      if (onError) onError(error);
    }
  }, [url, protocols, reconnectAttempt, maxReconnectAttempts, reconnectInterval, onOpen, onClose, onMessage, onError]);

  // Connect on mount, reconnect if URL changes
  useEffect(() => {
    connect();
    
    // Clean up on unmount
    return () => {
      if (reconnectTimeoutRef.current) {
        clearTimeout(reconnectTimeoutRef.current);
      }
      
      if (ws.current) {
        ws.current.close();
      }
    };
  }, [connect, url]);

  return {
    data,
    status,
    error,
    sendMessage,
    reconnectAttempt,
    isConnecting: status === 'CONNECTING',
    isOpen: status === 'OPEN',
    isClosed: status === 'CLOSED',
    isError: status === 'ERROR'
  };
}

export default useWebSocket;

Usage Example:

jsx
// components/ChatRoom.jsx
import { useState } from 'react';
import useWebSocket from '../hooks/useWebSocket';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');
  const [messages, setMessages] = useState([]);
  
  const { 
    sendMessage, 
    status, 
    data, 
    isOpen 
  } = useWebSocket(`wss://chat.example.com/room/${roomId}`, {
    onMessage: (data) => {
      setMessages(prev => [...prev, data]);
    }
  });
  
  const handleSend = (e) => {
    e.preventDefault();
    if (message.trim() && isOpen) {
      sendMessage(JSON.stringify({ text: message, timestamp: Date.now() }));
      setMessage('');
    }
  };
  
  return (
    <div className="chat-room">
      <div className="status">
        Connection status: {status}
      </div>
      
      <div className="messages">
        {messages.map((msg, index) => (
          <div key={index} className="message">
            <span className="author">{msg.author}:</span>
            <span className="text">{msg.text}</span>
            <span className="time">{new Date(msg.timestamp).toLocaleTimeString()}</span>
          </div>
        ))}
      </div>
      
      <form onSubmit={handleSend}>
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="Type a message..."
          disabled={!isOpen}
        />
        <button type="submit" disabled={!isOpen}>Send</button>
      </form>
    </div>
  );
}

Key Features:

  • 🔄 Automatic reconnection with configurable attempts
  • 📊 Connection state tracking
  • 🛡️ Error handling
  • 📨 Easy message sending with JSON support
  • 🧩 Customizable event handlers

8. useOnClickOutside

Detect clicks outside an element — commonly used for closing modals or dropdowns.

javascript
// hooks/useOnClickOutside.js
import { useEffect } from 'react';

function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = event => {
      // Do nothing if clicking ref's element or descendent elements
      if (!ref.current || ref.current.contains(event.target)) return;
      handler(event);
    };
    
    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);
    
    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

export default useOnClickOutside;

9. useHover

Track hover state for a DOM element.

javascript
// hooks/useHover.js
import { useState, useRef, useEffect } from 'react';

function useHover() {
  const [hovered, setHovered] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    const node = ref.current;
    if (!node) return;

    const handleMouseOver = () => setHovered(true);
    const handleMouseOut = () => setHovered(false);

    node.addEventListener('mouseover', handleMouseOver);
    node.addEventListener('mouseout', handleMouseOut);

    return () => {
      node.removeEventListener('mouseover', handleMouseOver);
      node.removeEventListener('mouseout', handleMouseOut);
    };
  }, []);

  return [ref, hovered];
}

export default useHover;

10. useMediaQuery

Use media queries in JS to react to device or screen changes.

javascript
// hooks/useMediaQuery.js
import { useState, useEffect } from 'react';

function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    // Create a media query list
    const media = window.matchMedia(query);
    // Set initial state
    setMatches(media.matches);

    // Define and add event listener
    const handler = (e) => setMatches(e.matches);
    media.addEventListener('change', handler);
    
    // Clean up
    return () => media.removeEventListener('change', handler);
  }, [query]);

  return matches;
}

export default useMediaQuery;

Usage Example:

jsx
// Using media queries for responsive design
const isMobile = useMediaQuery('(max-width: 768px)');

return (
  <div className={isMobile ? 'mobile-layout' : 'desktop-layout'}>
    {isMobile ? <MobileMenu /> : <DesktopMenu />}
  </div>
);

Conclusion

Custom hooks are one of the most powerful features in React, allowing you to extract and reuse stateful logic between components. The hooks covered in this guide will help you build more maintainable, efficient, and responsive React applications while reducing code duplication.

Feel free to create your own custom hooks when you encounter specific problems in your application.

Happy coding! 🚀

Similar articles

Never miss an update

Subscribe to receive news and special offers.

By subscribing you agree to our Privacy Policy.