17. Building Reusable Custom Hooks
Why Custom Hooks?
React hooks like useState and useEffect are powerful, but sometimes you’ll find yourself repeating the same logic in different components — like fetching data, handling input, or toggling modals. Custom hooks let you extract that logic into a reusable function.
- Encapsulate repeated logic
- Make components smaller and cleaner
- Improve readability and reusability
- Encourage consistent patterns across your app
Creating a Simple Custom Hook
A custom hook is just a regular JavaScript function that uses one or more built-in React hooks. By convention, it always starts with 'use'.
// useToggle.js
import { useState } from 'react'
export function useToggle(initial = false) {
const [value, setValue] = useState(initial)
const toggle = () => setValue(v => !v)
return [value, toggle]
}Now you can use `useToggle` anywhere in your app to easily handle open/close or on/off states.
// App.jsx
import { useToggle } from './useToggle'
function App() {
const [isOpen, toggleOpen] = useToggle(false)
return (
<div>
<button onClick={toggleOpen}>{isOpen ? 'Hide' : 'Show'} Details</button>
{isOpen && <p>Here are some extra details!</p>}
</div>
)
}
export default AppCustom Hook for Fetching Data
You can also build a hook to handle API calls — abstracting away repetitive useEffect and loading logic.
// useFetch.js
import { useState, useEffect } from 'react'
export function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let isMounted = true
fetch(url)
.then(res => res.json())
.then(json => isMounted && setData(json))
.catch(err => isMounted && setError(err))
.finally(() => isMounted && setLoading(false))
return () => { isMounted = false }
}, [url])
return { data, loading, error }
}This hook can now be reused across multiple components to handle any API endpoint.
// Users.jsx
import { useFetch } from './useFetch'
function Users() {
const { data: users, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users')
if (loading) return <p>Loading users...</p>
if (error) return <p>Error: {error.message}</p>
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
)
}
export default UsersBest Practices for Custom Hooks
- Prefix with `use` so React recognizes it as a hook
- Only call other hooks inside the custom hook (not conditionally)
- Keep them focused — one responsibility per hook
- Document expected parameters and return values
Mini Project Step
Extract your loading and fetching logic into a custom hook called `useFeatures`. It should fetch your feature list and return `data`, `loading`, and `error`. Use it inside your main component instead of directly calling React Query or Axios.
// src/hooks/useFeatures.js
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'
export function useFeatures() {
const fetchFeatures = async () => {
const res = await axios.get('https://jsonplaceholder.typicode.com/todos?_limit=3')
return res.data
}
const { data, isLoading, isError, error } = useQuery({ queryKey: ['features'], queryFn: fetchFeatures })
return { data, loading: isLoading, error: isError ? error : null }
}This makes your component simpler and your data fetching logic reusable in future lessons.