DEV Community

Cover image for Building an Authentication Wrapper in React/Next.js + GraphQL πŸ’ͺ
Sohail Jafri
Sohail Jafri

Posted on

Building an Authentication Wrapper in React/Next.js + GraphQL πŸ’ͺ

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
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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)
  },
})
Enter fullscreen mode Exit fullscreen mode

Note here Apollo Client provide handy callback functions like onError and onCompleted 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}</>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 (0)