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:
- Only call hooks at the top level - Never inside loops, conditions, or nested functions
- Only call hooks from React functions - Either React function components or custom hooks
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:
- React can’t track which state belongs to which hook
- State can get mixed up between different hooks
- Your component behavior becomes unpredictable
- You get runtime errors and crashes
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
- Synchronous hook function - The hook itself is not async
- Hooks at the top level - All hooks (useState, useEffect) are called at the function’s top level
- Async operations inside useEffect - The async logic is properly contained within useEffect
- Complete error handling - Proper error states and loading indicators
- Cleanup and state management - All state updates happen in the right order
Debugging Tips
When you encounter “Invalid hook call” errors:
- Check your hook calls - Make sure they’re at the top level
- Look for conditional hooks - Remove any hooks inside if statements or loops
- Verify async patterns - Ensure hooks aren’t called inside async functions
- Use ESLint - Install eslint-plugin-react-hooks to catch these errors automatically
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
- Hooks must be synchronous - Handle async operations inside useEffect
- Always call hooks at the top level - Never conditionally or in callbacks
- Provide loading and error states - Make your async operations user-friendly
- Use ESLint rules - Prevent these errors before they happen
- Remember the order matters - React tracks hooks by call order, not by name