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

17. Building Reusable Custom Hooks

Level: IntermediateDuration: 25m

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.