Code To Learn logo

Code To Learn

M5: Hooks & Performance

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:

src/pages/HomePage.jsx
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
}
src/pages/ListingDetailsPage.jsx
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:

Terminal
mkdir src/hooks
touch src/hooks/useFetch.js

This keeps all custom hooks organized in one place.

Import dependencies

src/hooks/useFetch.js
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

src/hooks/useFetch.js
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., /listings or /listings/123)

Add state variables

src/hooks/useFetch.js
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 data
  • isLoading - Loading state
  • error - Error message if request fails

Add fetch logic with useEffect

src/hooks/useFetch.js
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 url changes
  • Sets loading state before fetching
  • Catches and stores errors
  • Always sets loading to false (finally block)

Add AbortController cleanup

src/hooks/useFetch.js
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:

src/hooks/useFetch.js
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:

Example Usage
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:

Test in HomePage (temporary)
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:

  1. isLoading: true initially
  2. data: [...] with listings array
  3. isLoading: false when 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!