Logo
READLEARNKNOWCONNECT
Back to Lessons

    Page

  • - Why Custom Hooks?
  • - Creating a Simple Custom Hook
  • - Custom Hook for Fetching Data
  • - Best Practices for Custom Hooks
  • - Mini Project Step

17Building Reusable Custom Hooks

Intermediate25m

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'.

jsx
// 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.

jsx
// 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 App

Custom Hook for Fetching Data

You can also build a hook to handle API calls — abstracting away repetitive useEffect and loading logic.

jsx
// 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.

jsx
// 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 Users

Best 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.

jsx
// 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.

fetching data with react query and axios
unit testing