You've learned React's built-in hooks like useState, useEffect, and useContext. But the real power of hooks is that you can create your own! Custom hooks let you extract component logic into reusable functions. Instead of copying and pasting the same useState and useEffect combinations across components, you can package them into a custom hook and reuse it everywhere. Let's learn how to build your own hooks! šŖ
What Are Custom Hooks?
A custom hook is a JavaScript function whose name starts with "use" and that may call other hooks:
// A custom hook
function useWindowWidth() {
const [width, setWidth] = React.useState(window.innerWidth)
React.useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return width
}
// Use in any component
function MyComponent() {
const width = useWindowWidth()
return <div>Window width: {width}px</div>
}
// Key characteristics:
// 1. Name starts with "use"
// 2. Can call other hooks (useState, useEffect, etc.)
// 3. Returns whatever you want (value, object, array)
// 4. Reusable across componentsWhy "use" Prefix?
The "use" prefix is a React convention that:
⢠Tells React this function is a hook
⢠Enables React's linter to check hook rules
⢠Makes it clear this function uses React features
Without "use", React can't enforce hook rules!
The Problem: Duplicated Logic
Without custom hooks, you repeat the same logic in multiple components:
// ā Component 1: Duplicated logic
function UserProfile() {
const [user, setUser] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(null)
React.useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return <div>{user.name}</div>
}
// ā Component 2: Same logic copied!
function UserSettings() {
const [user, setUser] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(null)
React.useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return <div>{user.email}</div>
}
// Problems:
// 1. Same code in multiple places
// 2. Hard to maintain (fix bug in one, forget the other)
// 3. More code to write
// 4. Easy to make mistakesThe Solution: Extract to Custom Hook
Extract the shared logic into a custom hook:
// ā
Custom hook - Reusable logic
function useUser() {
const [user, setUser] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(null)
React.useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [])
return { user, loading, error }
}
// ā
Component 1: Clean and simple
function UserProfile() {
const { user, loading, error } = useUser()
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return <div>{user.name}</div>
}
// ā
Component 2: Same hook, no duplication
function UserSettings() {
const { user, loading, error } = useUser()
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return <div>{user.email}</div>
}
// Benefits:
// 1. Logic written once, used many times
// 2. Fix bugs in one place
// 3. Less code in components
// 4. Easier to test and maintainRules for Custom Hooks
- Name must start with "use"
// ā
Good
function useWindowWidth() { }
function useLocalStorage() { }
function useFetch() { }
// ā Bad
function getWindowWidth() { } // Missing "use"
function windowWidth() { } // Missing "use"
function WindowWidth() { } // Component, not hook- Can only call hooks at the top level
function useCustomHook() {
// ā
Good - Top level
const [state, setState] = React.useState(0)
// ā Bad - Inside condition
if (condition) {
const [count, setCount] = React.useState(0)
}
// ā Bad - Inside loop
for (let i = 0; i < 5; i++) {
React.useEffect(() => {})
}
return state
}- Can call other hooks
function useCustomHook() {
// ā
Can call any React hooks
const [state, setState] = React.useState(0)
const ref = React.useRef(null)
React.useEffect(() => {
// Effect logic
}, [])
// ā
Can call other custom hooks
const data = useAnotherCustomHook()
return state
}ā ļø Hook Rules Apply
Custom hooks must follow the same rules as built-in hooks:
⢠Only call at top level
⢠Only call from React functions
⢠Can call other hooks
The ESLint plugin checks these rules automatically!
Common Custom Hook Patterns
1. useFetch - Data Fetching
function useFetch(url) {
const [data, setData] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(null)
React.useEffect(() => {
let ignore = false
setLoading(true)
setError(null)
fetch(url)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
})
.then(data => {
if (!ignore) {
setData(data)
setLoading(false)
}
})
.catch(err => {
if (!ignore) {
setError(err.message)
setLoading(false)
}
})
return () => {
ignore = true
}
}, [url])
return { data, loading, error }
}
// Usage:
function UserList() {
const { data: users, loading, error } = useFetch('/api/users')
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}2. useLocalStorage - Persistent State
function useLocalStorage(key, initialValue) {
// Get initial value from localStorage or use initialValue
const [storedValue, setStoredValue] = React.useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(error)
return initialValue
}
})
// Return wrapped version of setState that also updates localStorage
const setValue = (value) => {
try {
// Allow value to be a function like useState
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error(error)
}
}
return [storedValue, setValue]
}
// Usage: Just like useState, but persists!
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
)
}
// Theme persists across page refreshes!3. useToggle - Boolean State
function useToggle(initialValue = false) {
const [value, setValue] = React.useState(initialValue)
const toggle = React.useCallback(() => {
setValue(v => !v)
}, [])
return [value, toggle]
}
// Usage:
function Modal() {
const [isOpen, toggleOpen] = useToggle(false)
return (
<div>
<button onClick={toggleOpen}>Toggle Modal</button>
{isOpen && (
<div className="modal">
<p>Modal content</p>
<button onClick={toggleOpen}>Close</button>
</div>
)}
</div>
)
}
// Simpler than useState for boolean toggles!4. useDebounce - Delayed Value
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = React.useState(value)
React.useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
// Usage:
function SearchBox() {
const [searchTerm, setSearchTerm] = React.useState('')
const debouncedSearchTerm = useDebounce(searchTerm, 500)
React.useEffect(() => {
if (debouncedSearchTerm) {
// API call here
console.log('Searching for:', debouncedSearchTerm)
}
}, [debouncedSearchTerm])
return (
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
)
}
// Only searches after user stops typing for 500ms!5. usePrevious - Previous Value
function usePrevious(value) {
const ref = React.useRef()
React.useEffect(() => {
ref.current = value
})
return ref.current
}
// Usage:
function Counter() {
const [count, setCount] = React.useState(0)
const prevCount = usePrevious(count)
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}6. useOnClickOutside - Click Detection
function useOnClickOutside(ref, handler) {
React.useEffect(() => {
const listener = (event) => {
// Do nothing if clicking ref's element or descendants
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])
}
// Usage:
function Dropdown() {
const [isOpen, setIsOpen] = React.useState(false)
const dropdownRef = React.useRef(null)
useOnClickOutside(dropdownRef, () => setIsOpen(false))
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>Menu</button>
{isOpen && (
<ul>
<li>Option 1</li>
<li>Option 2</li>
</ul>
)}
</div>
)
}
// Dropdown closes when clicking outside!Interactive Demo
š” Tip: Edit the code above and click "Run" to see your changes
Advanced Custom Hook Patterns
Pattern 1: Hook with Parameters
function useFetch(url, options = {}) {
const [data, setData] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(null)
React.useEffect(() => {
let ignore = false
fetch(url, options)
.then(res => res.json())
.then(data => {
if (!ignore) setData(data)
})
.catch(err => {
if (!ignore) setError(err)
})
.finally(() => {
if (!ignore) setLoading(false)
})
return () => {
ignore = true
}
}, [url, options]) // Re-fetch when url or options change
return { data, loading, error }
}
// Usage with different parameters:
const users = useFetch('/api/users')
const posts = useFetch('/api/posts', { method: 'POST' })Pattern 2: Hook Returning Multiple Functions
function useCounter(initialValue = 0) {
const [count, setCount] = React.useState(initialValue)
const increment = React.useCallback(() => setCount(c => c + 1), [])
const decrement = React.useCallback(() => setCount(c => c - 1), [])
const reset = React.useCallback(() => setCount(initialValue), [initialValue])
const set = React.useCallback((value) => setCount(value), [])
return {
count,
increment,
decrement,
reset,
set
}
}
// 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>
)
}Pattern 3: Composing Hooks
// Base hooks
function useLocalStorage(key, initialValue) {
// ... implementation
}
function useMedia(query) {
const [matches, setMatches] = React.useState(
window.matchMedia(query).matches
)
React.useEffect(() => {
const media = window.matchMedia(query)
const listener = () => setMatches(media.matches)
media.addListener(listener)
return () => media.removeListener(listener)
}, [query])
return matches
}
// Composed hook using both
function useDarkMode() {
const [enabledState, setEnabledState] = useLocalStorage('dark-mode', false)
const prefersDarkMode = useMedia('(prefers-color-scheme: dark)')
const enabled = enabledState ?? prefersDarkMode
React.useEffect(() => {
document.body.classList.toggle('dark-mode', enabled)
}, [enabled])
return [enabled, setEnabledState]
}
// Usage: One hook with combined functionality!
function App() {
const [darkMode, setDarkMode] = useDarkMode()
return (
<button onClick={() => setDarkMode(!darkMode)}>
Toggle {darkMode ? 'Light' : 'Dark'} Mode
</button>
)
}Custom Hook Best Practices
- Name with "use" prefix - Required for hook rules
- Keep hooks focused - One responsibility per hook
- Return what's useful - State, functions, or both
- Use array/object returns - Array for simple, object for many values
- Document parameters - JSDoc comments help users
- Handle edge cases - Null checks, error handling
- Make hooks composable - Can use other hooks
- Memoize callbacks - Use useCallback for functions
- Test custom hooks - Use React Testing Library
- Extract when you see duplication - Don't over-abstract
When to Create a Custom Hook:
Create a custom hook when:
⢠You copy-paste the same useState/useEffect pattern
⢠Logic is used in 2+ components
⢠Complex logic clutters your component
⢠You want to share logic across projects
Don't create one for:
⢠One-time use cases
⢠Very simple logic (one useState)
⢠When a helper function would work
Common Custom Hook Mistakes
1. Forgetting "use" Prefix
// ā Wrong - Missing "use"
function fetchData() {
const [data, setData] = React.useState(null)
// ... React won't enforce hook rules!
return data
}
// ā
Correct - Starts with "use"
function useFetchData() {
const [data, setData] = React.useState(null)
// ... Hook rules enforced
return data
}2. Not Handling Cleanup
// ā Wrong - No cleanup
function useInterval(callback, delay) {
React.useEffect(() => {
const id = setInterval(callback, delay)
// Missing cleanup!
}, [callback, delay])
}
// ā
Correct - Cleanup interval
function useInterval(callback, delay) {
React.useEffect(() => {
const id = setInterval(callback, delay)
return () => clearInterval(id) // Cleanup!
}, [callback, delay])
}3. Creating New Objects in Dependencies
// ā Wrong - options object recreated every render
function useFetch(url, options) {
React.useEffect(() => {
fetch(url, options)
}, [url, options]) // options is always "new"!
}
// ā
Better - Destructure specific values
function useFetch(url, method = 'GET') {
React.useEffect(() => {
fetch(url, { method })
}, [url, method]) // Only re-fetch when method changes
}
// ā
Or use useMemo
function Component() {
const options = React.useMemo(() => ({
method: 'POST'
}), [])
const data = useFetch('/api/data', options)
}Key Takeaways
- Custom hooks extract and reuse component logic
- Name must start with "use" prefix
- Can call other React hooks and custom hooks
- Must follow all hook rules (top level, React functions only)
- Return values, functions, or objects as needed
- Great for data fetching, localStorage, toggle state, etc.
- Reduces code duplication across components
- Makes logic testable in isolation
- Can compose multiple hooks together
- Each hook should have one clear responsibility
- Document parameters and return values
- Handle cleanup in useEffect properly
- Custom hooks are functions, not components
What's Next?
You now know how to create custom hooks to extract and reuse component logic! You can build a library of reusable hooks for common patterns.
In the next lesson, you'll learn about Higher-Order Components (HOCs):
- What are Higher-Order Components
- Creating HOCs to enhance components
- Common HOC patterns
- When to use HOCs vs hooks
- HOC best practices
Get ready to learn another powerful React pattern! š