React Router: “Invalid hook call” when using higher-order components for authentication
Problem
I have two different implementations of authentication wrappers for my React components using React Router. The first implementation works correctly, while the second one causes an “Invalid hook call” error. I don’t understand why this is happening.
Working Implementation
import { PropsWithChildren } from 'react'
import { Navigate } from 'react-router-dom'
import { useAuth } from './auth-provider'
export function WithOptimisticAuth(props: PropsWithChildren) {
const { isAuthenticated, isLoading, isError } = useAuth()
if (isError || (!isLoading && isAuthenticated === false)) return <Navigate to="/login" replace />
return <>{props.children}</>
}
export function WithConfirmedAuth(props: PropsWithChildren) {
const { isAuthenticated, isLoading, isError } = useAuth()
if (isLoading) return <></>
if (isError || isAuthenticated === false) return <Navigate to="/login" replace />
return <>{props.children}</>
}
Usage in router configuration:
// ... (router configuration)
.with(Routes.Installations, () => ({
path: Routes.Installations,
element: (
<WithOptimisticAuth>
<InstallationsPage />
</WithOptimisticAuth>
),
}))
// ... (other routes)
Problematic Implementation
import { ComponentType, PropsWithChildren } from 'react'
import { Navigate } from 'react-router-dom'
import { useAuth } from './auth-provider'
export const withOptimisticAuth =
<P extends object>(Component: ComponentType<P>) =>
(props: PropsWithChildren<P>) => {
const { isAuthenticated, isLoading, isError } = useAuth()
if (isError || (!isLoading && isAuthenticated === false)) return <Navigate to="/login" replace />
return <Component {...props} />
}
export const withConfirmedAuth =
<P extends object>(Component: ComponentType<P>) =>
(props: PropsWithChildren<P>) => {
const { isAuthenticated, isLoading, isError } = useAuth()
if (isLoading) return <></>
if (isError || isAuthenticated === false) return <Navigate to="/login" replace />
return <Component {...props} />
}
Usage in router configuration:
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
errorElement: <ErrorPage />,
children: Object.keys(Routes).map(route =>
match<Route>(Routes[route as keyof typeof Routes])
.with(Routes.Installations, () => ({
path: Routes.Installations,
element: withOptimisticAuth(InstallationsPage)({}),
}))
// ... (other routes)
Error Message
When using the second implementation, I get the following error:
Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
Question
Why does the second implementation cause an “Invalid hook call” error, while the first one works correctly? How can I fix the second implementation to work properly?
withOptimisticAuth(InstallationsPage)
returns a functional component, then I render that component by calling the function withOptimisticAuth(InstallationsPage)({})
.
How is it different from inlining the HOC functionality in InstallationsPage
:
export function InstallationsPage() {
const { isAuthenticated, isLoading, isError } = useAuth()
if (isError || (!isLoading && isAuthenticated === false)) return <Navigate to="/login" replace />
And rendering the component directly:
element: <InstallationsPage />
Which works fine, but using the HOC doesn’t work.