A custom hook is a JavaScript function whose name starts with use and which can call other React hooks such as useState, useEffect, useRef, or useContext. The main purpose of a custom hook is to extract reusable stateful logic from components.
Many React applications eventually contain repeated patterns such as fetching data, tracking online status, reading from local storage, debouncing user input, or handling form fields. If the same logic appears in multiple components, a custom hook allows you to move that logic into one reusable function and use it wherever needed.
A custom hook does not share rendered UI. It shares logic. Two components can use the same custom hook and still render completely different layouts.
Custom hooks are one of the cleanest ways to organize React code. Instead of placing every effect, event listener, timer, and utility state directly inside components, you can move related logic into one small reusable unit.
| Feature | Normal function | Custom hook |
|---|---|---|
| Name | Any valid function name | Must start with use |
| Can call React hooks? | No | Yes |
| Main purpose | General JavaScript logic | Reusable React stateful logic |
| Used inside components | Yes | Yes |
| Knows about React lifecycle | No | Yes, through hooks like useEffect |
If a function does not call React hooks, it is usually just a normal helper function. If it uses hooks and follows React hook rules, it should be written as a custom hook.
A custom hook follows the same rules as built-in hooks:
useReact depends on hooks being called in the same order on every render. That is why these rules matter. A custom hook is not a special exception; it must follow the same behavior as any built-in hook.
Suppose two components need to detect whether the browser is online or offline. Without a custom hook, both components might repeat the same state and effect code.
import { useEffect, useState } from 'react'
function StatusBanner() {
const [isOnline, setIsOnline] = useState(navigator.onLine)
useEffect(() => {
const goOnline = () => setIsOnline(true)
const goOffline = () => setIsOnline(false)
window.addEventListener('online', goOnline)
window.addEventListener('offline', goOffline)
return () => {
window.removeEventListener('online', goOnline)
window.removeEventListener('offline', goOffline)
}
}, [])
return <p>{isOnline ? 'You are online' : 'You are offline'}</p>
}
import { useEffect, useState } from 'react'
function CheckoutButton() {
const [isOnline, setIsOnline] = useState(navigator.onLine)
useEffect(() => {
const goOnline = () => setIsOnline(true)
const goOffline = () => setIsOnline(false)
window.addEventListener('online', goOnline)
window.addEventListener('offline', goOffline)
return () => {
window.removeEventListener('online', goOnline)
window.removeEventListener('offline', goOffline)
}
}, [])
return (
<button disabled={!isOnline}>
{isOnline ? 'Place Order' : 'Offline'}
</button>
)
}
Both components contain the same state and side-effect logic. This is a perfect situation for a custom hook.
We can move the repeated logic into useOnlineStatus(). Then any component can ask for the current online status without rewriting the event listener code.
import { useEffect, useState } from 'react'
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine)
useEffect(() => {
const goOnline = () => setIsOnline(true)
const goOffline = () => setIsOnline(false)
window.addEventListener('online', goOnline)
window.addEventListener('offline', goOffline)
return () => {
window.removeEventListener('online', goOnline)
window.removeEventListener('offline', goOffline)
}
}, [])
return isOnline
}
import { useOnlineStatus } from './hooks/useOnlineStatus'
function StatusBanner() {
const isOnline = useOnlineStatus()
return <p>{isOnline ? 'You are online' : 'You are offline'}</p>
}
function CheckoutButton() {
const isOnline = useOnlineStatus()
return (
<button disabled={!isOnline}>
{isOnline ? 'Place Order' : 'Offline'}
</button>
)
}
Notice what happened here: the components became much smaller, and the logic became reusable. That is the core benefit of custom hooks.
A good custom hook usually does one small job well. It takes inputs as arguments, uses internal state or effects when needed, and returns the values or functions that components need.
This makes a custom hook feel similar to a small reusable API for component logic.
Fetching data is one of the most common custom hook use cases. The hook can manage loading state, errors, cleanup, and the fetched result in one place.
import { useEffect, useState } from 'react'
export function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
if (!url) return
const controller = new AbortController()
async function loadData() {
try {
setLoading(true)
setError(null)
const response = await fetch(url, { signal: controller.signal })
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`)
}
const json = await response.json()
setData(json)
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message)
}
} finally {
setLoading(false)
}
}
loadData()
return () => controller.abort()
}, [url])
return { data, loading, error }
}
import { useFetch } from './hooks/useFetch'
function UserList() {
const { data: users, loading, error } = useFetch('/api/users')
if (loading) return <p>Loading users...</p>
if (error) return <p>Error: {error}</p>
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
import { useFetch } from './hooks/useFetch'
function PostList() {
const { data: posts, loading, error } = useFetch('/api/posts')
if (loading) return <p>Loading posts...</p>
if (error) return <p>Error: {error}</p>
return <p>Total posts: {posts?.length ?? 0}</p>
}
The biggest advantage here is that the fetch logic lives in one place. If you later want to improve error handling, add retry support, or include headers, you can update the hook rather than changing many components.
A custom hook can also connect React state to browser APIs. One common example is keeping a value in localStorage so it survives page refreshes.
import { useEffect, useState } from 'react'
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const savedValue = localStorage.getItem(key)
return savedValue ? JSON.parse(savedValue) : initialValue
} catch {
return initialValue
}
})
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value))
} catch (err) {
console.error('Unable to save value:', err)
}
}, [key, value])
const removeValue = () => {
localStorage.removeItem(key)
setValue(initialValue)
}
return [value, setValue, removeValue]
}
import { useLocalStorage } from './hooks/useLocalStorage'
function SettingsPanel() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
const [fontSize, setFontSize, clearFontSize] = useLocalStorage('fontSize', 16)
return (
<div>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Theme: {theme}
</button>
<input
type="range"
min={12}
max={24}
value={fontSize}
onChange={e => setFontSize(Number(e.target.value))}
/>
<p style={{ fontSize }}>Preview text size: {fontSize}px</p>
<button onClick={clearFontSize}>Reset Font Size</button>
</div>
)
}
This hook hides the storage details. The component simply works with a state-like API.
Debouncing means delaying an update until the user stops typing for a short time. This is useful for search boxes, filters, and API requests.
import { useEffect, useState } from 'react'
export function useDebounce(value, delay = 400) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
import { useEffect, useState } from 'react'
import { useDebounce } from './hooks/useDebounce'
function SearchBox() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 500)
useEffect(() => {
if (!debouncedQuery) return
console.log('Run API search for:', debouncedQuery)
}, [debouncedQuery])
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search products"
/>
<p>Immediate value: {query}</p>
<p>Debounced value: {debouncedQuery}</p>
</div>
)
}
The component still owns the input field, but the hook gives it a delayed version of the value. That makes the UI simpler and prevents repeated timer code in different components.
Not every custom hook needs to be large. Some of the most useful hooks are tiny. A boolean toggle hook is a simple example.
import { useState } from 'react'
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue)
const toggle = () => setValue(current => !current)
const setTrue = () => setValue(true)
const setFalse = () => setValue(false)
return { value, toggle, setTrue, setFalse }
}
import { useToggle } from './hooks/useToggle'
function ModalExample() {
const { value: isOpen, toggle, setFalse } = useToggle(false)
return (
<div>
<button onClick={toggle}>
{isOpen ? 'Hide' : 'Show'} Modal
</button>
{isOpen && (
<div className="modal">
<p>This modal uses a custom hook for state management.</p>
<button onClick={setFalse}>Close</button>
</div>
)}
</div>
)
}
A custom hook is just a function, so it can return any structure that makes sense:
isOnline[value, setValue]{ data, loading, error }toggle, reset, or removeValueChoose the return style that makes the hook easiest to understand. Arrays work well when the order is obvious. Objects work well when many named values are returned.
Custom hooks can call other custom hooks. This makes it possible to build higher-level logic from smaller reusable pieces.
import { useDebounce } from './useDebounce'
import { useFetch } from './useFetch'
export function useSearchResults(query) {
const debouncedQuery = useDebounce(query, 500)
const url = debouncedQuery ? `/api/search?q=${encodeURIComponent(debouncedQuery)}` : null
const { data, loading, error } = useFetch(url)
return {
results: data ?? [],
loading,
error,
debouncedQuery
}
}
This style is powerful because it lets you create app-specific hooks from smaller generic hooks.
You do not need a custom hook for every tiny line of code. It usually makes sense to create one when:
If the logic is used only once and is already easy to read inside the component, you may not need a custom hook yet.
| Mistake | Why it is a problem | Better approach |
|---|---|---|
Naming it without use | React hook rules and linting will not recognize it correctly | Use names like useFetch or useToggle |
| Calling hooks conditionally | Breaks hook order between renders | Call hooks at the top level every time |
| Putting too much unrelated logic into one hook | Makes the hook hard to reuse and maintain | Keep each hook focused on one responsibility |
| Returning confusing values | Consumers will misuse the hook | Return a clear array or object with meaningful names |
| Using a custom hook only to wrap one line with no benefit | Adds extra abstraction without improving readability | Create hooks where they clearly improve reuse or clarity |
You can think of a component as answering the question, "What should the UI look like?" A custom hook answers the question, "How should this behavior work?" When you separate those concerns well, React code becomes much easier to read.
Custom hooks are one of the most important patterns in modern React. They allow you to extract reusable stateful logic, keep components small, and organize effects and state in a much cleaner way. Whether you are fetching data, synchronizing with local storage, debouncing user input, listening to browser events, or managing reusable UI behavior, a good custom hook can remove duplication and make your code easier to understand.
The key idea is simple: if multiple components need the same hook-based logic, move that logic into a function that starts with use, follow the rules of hooks, and return the values the component needs. Over time, custom hooks help a React codebase become more modular, readable, and maintainable.
Explore 500+ free tutorials across 20+ languages and frameworks.