Skip to content

Invalid Hook Call - React Hooks Fundamentals

Published: at 05:08 PM

Have you ever encountered this dreaded React error?

Error: "Invalid hook call. Hooks can only be called inside the 
body of a function component."

If you’re reading this, chances are you have. This error is React’s way of telling you that you’ve violated one of the fundamental Rules of Hooks. Let me show you exactly what causes this error and how to fix it.

The Problem: A Broken Custom Hook

Here’s an example of what NOT to do when creating a custom hook:

export async function useUserPermissions() {
   const user = await fetchCurrentUser()  // async operation

   if (!user) {
       return { hasPermission: false }
   }

   const [hasPermission, setHasPermission] = useState(false)  // hook after condition
   
   useEffect(() => {
     // API call logic...
   }, [])
   
   return { hasPermission }
}

And calling it like:

useEffect(() => {
    const checkPermissions = async () => {
      const result = await useUserPermissions()  // hook inside another hook and async func
      setUserPermissions(result)
    }
    checkPermissions()
}, [])

This code breaks React’s Rules of Hooks in three different ways. Let’s understand why.

The Rules of Hooks

React has two fundamental rules that must always be followed:

Breaking Down the Violations

Violation #1: Async Hook Function

export async function useUserPermissions() {
  // Hooks cannot be async functions
}

Why this fails: React expects hooks to be synchronous and return values immediately. When you make a hook async, it returns a Promise instead of the expected hook return value.

Violation #2: Conditional Hook Calls

const user = await fetchCurrentUser()
if (!user) {
  return { hasPermission: false } // Early return
}
const [hasPermission, setHasPermission] = useState(false) // Hook after condition

Why this fails: React tracks hooks by their call order. If hooks are called conditionally, React can’t maintain consistent state between renders.

Violation #3: Hooks in Async Functions

useEffect(() => {
  const checkPermissions = async () => {
    const result = await useUserPermissions() // Hook inside another hook and async callback
  }
  checkPermissions()
}, [])

Why this fails: The hook is being called inside an async function, which violates the “top level only” rule.

Why These Rules Matter

React uses the order of hook calls to associate state with each hook. When you break these rules:

The Correct Way

Here’s how to properly structure the custom hook:

export function useUserPermissions() {
    const [hasPermission, setHasPermission] = useState<boolean>(false)
    const [loading, setLoading] = useState<boolean>(true)
    const [error, setError] = useState<string | null>(null)

    useEffect(() => {
        const checkPermissions = async () => {
            try {
                setLoading(true)
                setError(null)
                const user = await fetchCurrentUser()

                if (!user) {
                    setHasPermission(false)
                    return
                }

                const response = await fetch(`/api/users/${user.id}/permissions`)
                if(!response.ok) {
                    throw new Error('HTTP error: ', response.status)
                }

                const permissionData = await response.json()

                const hasRequiredPermission = permissionData.roles.includes('admin') ||
                                                permissionData.permissions.includes('read:users')
                setHasPermission(hasRequirdPermission)

            } catch (err) {
                const errorMsg = err instanceof Error ? err.message : 'Failed to check permissions'
                setError('Failed to check permissions: ', errorMsg)
                setHasPermission(false)
                
            } finally {
                setLoading(false)

            }
        }

        checkPermissions()
    }, [])

    return { hasPermission, loading, error }
}

And using it in your component:

function MyComponent() {
  const { hasPermission, loading, error } = useUserPermissions()

  if (loading) return <div>Checking permissions...</div>
  if (error) return <div>Error: {error}</div>

  return (
    <div>
      {hasPermission ? (
        <div>You have access!</div>
      ) : (
        <div>Access denied</div>
      )}
    </div>
  )
}

What Makes This Solution Correct

Debugging Tips

When you encounter “Invalid hook call” errors:

Prevention with ESLint

Add this to your ESLint configuration to catch hook violations:

{
  "extends": ["react-hooks"],
  "rules": {
    "react-hooks/recommended-latest": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

Key Takeaways