Managing authentication in a React/Next.js application with GraphQL can sometimes be tricky, especially when you need to protect routes and manage user sessions efficiently. In this article, we'll walk through setting up an AuthWrapper
component to handle authentication seamlessly in your application.
Prerequisites
Before diving in, ensure you have the following:
- A Next.js project set up.
- A backend or API with GraphQL support for authentication.
- Basic understanding of React hooks like
useEffect
and Apollo Client or any other GraphQL client.
The Goal
We want to create a reusable AuthWrapper
component that:
- Protects routes by redirecting unauthenticated users.
- Fetches authenticated user data (e.g., customer details) after login.
- Shows a loader during the authentication process.
Setting Up the AuthWrapper
Component
Hereβs the updated implementation of the AuthWrapper
component:
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import { useQuery } from '@apollo/client'
import { useAuth } from '@/hooks/useAuth'
import Loader from '@/components/Loader'
import { GET_USER_ME_QY } from '@/lib/graphql'
import { UserDocument } from '@/types'
const authTokenName = 'authToken'
interface Props {
children: React.ReactNode
}
const AuthWrapper = ({ children }: Props) => {
const router = useRouter()
const { setIsAuth, setUser, isAuthing, setIsAuthing } = useAuth()
// Redirect to login if no auth token exists
useEffect(() => {
if (typeof window === 'undefined') return
if (localStorage.getItem(authTokenName) === null) {
router.push('/login')
}
}, [router])
// Query to fetch authenticated user data
const { refetch, loading } = useQuery<{
userMe: UserDocument
}>(GET_USER_ME_QY, {
skip:
typeof window === 'undefined' ||
localStorage.getItem(authTokenName) === null,
fetchPolicy: 'network-only',
onError: (error) => {
console.error('Error fetching user me', error)
setIsAuth(false)
setIsAuthing(false)
},
onCompleted: (data) => {
setIsAuth(true)
setIsAuthing(false)
setUser(data.userMe)
},
})
// Trigger refetch if authentication token exists
useEffect(() => {
if (
typeof window !== 'undefined' &&
localStorage.getItem(authTokenName) !== null
) {
refetch()
}
}, [refetch])
return isAuthing ? <Loader /> : <>{children}</>
}
export default AuthWrapper
Key Explanations
1. Authentication Check
This is crucial for protecting your routes. If a user tries to access a protected page without being authenticated, they will be seamlessly redirected to the login page, enhancing the user experience.
useEffect(() => {
if (localStorage.getItem(authTokenName) === null) {
router.push('/login')
}
}, [router])
2. Fetching Authenticated User Data
The useQuery hook fetches the authenticated user data using the GET_USER_ME_QY
query:
const { refetch, loading } = useQuery<{
userMe: UserDocument
}>(GET_USER_ME_QY, {
skip:
typeof window === 'undefined' ||
localStorage.getItem(authTokenName) === null,
fetchPolicy: 'network-only',
onError: (error) => {
console.error('Error fetching user me', error)
setIsAuth(false)
setIsAuthing(false)
},
onCompleted: (data) => {
setIsAuth(true)
setIsAuthing(false)
setUser(data.userMe)
},
})
Note here Apollo Client provide handy callback functions like
onError
andonCompleted
to handle errors and data respectively. But you can use your own error and success handling logic.
3. Loader for Authentication Process
Loader
provides visual feedback to users, indicating that their authentication status is being verified, which is essential for a smooth user experience.
return isAuthing ? <Loader /> : <>{children}</>
4. State Management
We use a custom useAuth
hook to manage authentication state across the app. Below is the implementation of the useAuth
hook:
import { useState, useContext, createContext } from 'react'
import { UserDocument } from '@/types'
const AuthContext = createContext(null)
export const AuthProvider = ({ children }) => {
const [isAuthing, setIsAuthing] = useState(true)
const [isAuth, setIsAuth] = useState(false)
const [user, setUser] = useState<UserDocument | null>(null)
return (
<AuthContext.Provider
value={{
isAuthing,
setIsAuthing,
isAuth,
setIsAuth,
user,
setUser,
}}
>
{children}
</AuthContext.Provider>
)
}
export const useAuth = () => useContext(AuthContext)
Integrating AuthWrapper in Your App
To use the AuthWrapper, wrap your app or specific pages that require authentication:
import AuthWrapper from '../components/AuthWrapper'
const ProtectedPage = () => {
return (
<AuthWrapper>
<h1>You must be logged in to see me user!</h1>
</AuthWrapper>
)
}
export default ProtectedPage
Conclusion
Building an authentication wrapper in React/Next.js with GraphQL can help streamline your app's authentication process. By following the steps outlined in this article, you can create a reusable AuthWrapper component that handles authentication, protects routes, and fetches user data efficiently.
Get In Touch
Feel free to share your thoughts or ask for further clarification by reaching out to me. Happy coding! Hack on!
Top comments (8)
This is an incredibly detailed and well-explained guide! The use of useQuery for fetching user data with Apollo Client is particularly elegant. Thanks for sharing! π
This looks super clean and reusable! The idea of combining GraphQL with React hooks for auth management is brilliant. Great job! π‘
Can you elaborate on how error handling is managed for edge cases, like a network failure or malformed token? Would love to see some fallback mechanisms!
Your example is clear and makes implementing this so much easier
111
Hehe Thanks Man for commenting
This is amazing, but what happens if the token expires while the user is still on a protected page? Does it revalidate or force a logout?
Love how you've incorporated Loader for a better user experience during the authentication process