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

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.
// 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:
// 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.
// 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.
// 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:
// 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.
// 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.
// 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.
// 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.
// 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:
// 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.
// 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.
// 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.
// 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:
// 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! 🚀