L1: Custom Hook Basics
Learn to create reusable custom hooks to share logic between components
React custom hooks let you extract component logic into reusable functions. They're one of React's most powerful features for keeping code DRY (Don't Repeat Yourself)!
What You'll Learn
- What custom hooks are
- Rules for creating hooks
- Extract useFetch custom hook
- Share logic between components
- Clean up duplicate code
The Problem: Duplicate Code
Right now, both HomePage and ListingDetailsPage have nearly identical data fetching logic:
export function HomePage() {
const [listings, setListings] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchListings = async () => {
try {
setIsLoading(true);
const response = await api.get('/listings');
setListings(response.data);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchListings();
}, []);
// ... rest of component
}export function ListingDetailsPage() {
const { id } = useParams();
const [listing, setListing] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchListing = async () => {
try {
setIsLoading(true);
const response = await api.get(`/listings/${id}`);
setListing(response.data);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchListing();
}, [id]);
// ... rest of component
}Notice the pattern? Almost identical state management, error handling, and loading logic. This is a perfect use case for a custom hook!
What Are Custom Hooks?
Custom hooks are JavaScript functions that use React hooks internally. They let you extract component logic into reusable functions that can be shared across multiple components.
Benefits:
- ✅ Eliminate duplicate code
- ✅ Share logic between components
- ✅ Easier testing and maintenance
- ✅ Better code organization
- ✅ Compose complex behaviors
Rules of Custom Hooks
Custom hooks must follow these rules:
Creating useFetch Hook
Let's create a useFetch custom hook that handles all our data fetching logic!
Create hooks directory
First, create a new folder for your custom hooks:
mkdir src/hooks
touch src/hooks/useFetch.jsThis keeps all custom hooks organized in one place.
Import dependencies
import { useState, useEffect } from 'react';
import api from '@/api';We need useState for state management, useEffect for side effects, and our api instance for requests.
Define the hook function
import { useState, useEffect } from 'react';
import api from '@/api';
export function useFetch(url) {
// Hook implementation will go here
}Parameters:
url- The API endpoint to fetch from (e.g.,/listingsor/listings/123)
Add state variables
export function useFetch(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
// Effect will go here
return { data, isLoading, error };
}Same three states we use everywhere:
data- The fetched dataisLoading- Loading stateerror- Error message if request fails
Add fetch logic with useEffect
export function useFetch(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true);
setError(null);
const response = await api.get(url);
setData(response.data);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [url]);
return { data, isLoading, error };
}Key points:
- Runs whenever
urlchanges - Sets loading state before fetching
- Catches and stores errors
- Always sets loading to false (finally block)
Add AbortController cleanup
export function useFetch(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setIsLoading(true);
setError(null);
const response = await api.get(url, {
signal: controller.signal
});
setData(response.data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setIsLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, isLoading, error };
}Prevents race conditions by canceling in-flight requests when:
- Component unmounts
- URL changes
Complete Hook Code
Here's the full useFetch hook:
import { useState, useEffect } from 'react';
import api from '@/api';
export function useFetch(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setIsLoading(true);
setError(null);
const response = await api.get(url, {
signal: controller.signal
});
setData(response.data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setIsLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, isLoading, error };
}How to Use It
Now you can use this hook in any component:
import { useFetch } from '@/hooks/useFetch';
function MyComponent() {
const { data, isLoading, error } = useFetch('/api/endpoint');
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
}One line replaces 20+ lines of fetch logic! 🎉
Benefits Recap
// Duplicated in multiple components
function Component() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setIsLoading(true);
const response = await api.get(url, {
signal: controller.signal
});
setData(response.data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setIsLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
// ... rest
}Problems:
- 25+ lines of boilerplate
- Copy-pasted everywhere
- Hard to maintain
- Easy to forget cleanup
// Clean and reusable
function Component() {
const { data, isLoading, error } = useFetch(url);
// ... rest
}Benefits:
- ✅ 1 line instead of 25+
- ✅ Logic in one place
- ✅ Easy to maintain
- ✅ Cleanup handled automatically
- ✅ Reusable across app
Testing Your Hook
Let's verify the hook works:
import { useFetch } from '@/hooks/useFetch';
export function HomePage() {
const { data, isLoading, error } = useFetch('/listings');
console.log('Hook data:', data);
console.log('Hook loading:', isLoading);
console.log('Hook error:', error);
// ... rest of component (keep existing code for now)
}Check your browser console - you should see:
isLoading: trueinitiallydata: [...]with listings arrayisLoading: falsewhen done
What's Next?
In Lesson 2, we'll refactor HomePage to use useFetch instead of manual fetch logic. We'll eliminate 25+ lines of duplicate code! 🚀
Summary
- ✅ Custom hooks start with
use - ✅ Extract reusable logic from components
- ✅ Can use other React hooks
- ✅ Each call gets isolated state
- ✅ Created useFetch for data fetching
- ✅ Eliminates code duplication
Key concept: Custom hooks are functions that use hooks - they're not magic, just a pattern for sharing logic!