Auth Context for Next.js (Cookie-Based Sessions)

A React context wrapping an API — login, logout, signup, and password reset — that keeps the client in sync with an httpOnly session cookie by revalidating on mount.

3 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

The auth context I reuse across Next.js Projects. It doesn't store a token anywhere in the client — every request goes out with credentials: 'include', so the actual session lives in an httpOnly cookie sets on login. On mount, it calls /api/users/me once to ask the server "am I still logged in?" and syncs user/status from that answer, instead of trusting anything cached client-side.

providers/AuthContext.tsx
'use client'
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { User } from 'types'
 
 
 
const Context = createContext({} as AuthContext)
 
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [user, setUser] = useState<User | null>()
  // Tracks the single event of logging in or out — useful for effects
  // that should only run once when auth state actually changes.
  const [status, setStatus] = useState<undefined | 'loggedOut' | 'loggedIn'>()
 
  const create = useCallback<Create>(async args => {
//createAPILogic
  }, [])
 
  const login = useCallback<Login>(async args => {
    try {
   setUser(user)
	setStatus('loggedIn')
    } catch (e) {
      throw new Error('An error occurred while attempting to login.')
    }
  }, [])
 
  const logout = useCallback<Logout>(async () => {
 
  }, [])
 
  // Revalidate: on mount, ask the server whether the httpOnly cookie
  // still represents a valid session, and sync local state to match.
  useEffect(() => {
    const fetchMe = async () => {
      try {
        //getUserMe with credentilas
        //setUser
      } catch (e) {
        setUser(null)
        throw new Error('An error occurred while fetching your account.')
      }
    }
    fetchMe()
  }, [])
 
  const forgotPassword = useCallback<ForgotPassword>(async args => {
    try {
      //forgetAPILogic
    } catch (e) {
      throw new Error('An error.')
    }
  }, [])
 
  const resetPassword = useCallback<ResetPassword>(async args => {
    try {
	//resetAPILogic
    } catch (e) {
      throw new Error('An error occurred while attempting to login.')
    }
  }, [])
 
  return (
    <Context.Provider
      value={{ 
      user,
      setUser,
      login,
      logout,
      create,
      resetPassword,
      forgotPassword,
      status 
      }}
    >
      {children}
    </Context.Provider>
  )
}
 
type UseAuth<T = User> = () => AuthContext
export const useAuth: UseAuth = () => useContext(Context)

Usage #

app/layout.tsx
import { AuthProvider } from '@/providers/AuthContext'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  )
}
components/LoginForm.tsx
function LoginForm() {
  const { login, user, status } = useAuth()
 
  const onSubmit = async (email: string, password: string) => {
    try {
      await login({ email, password })
    } catch (e) {
      // show a login error
    }
  }
 
  if (status === 'loggedIn') return <p>Welcome back, {user?.email}</p>
 
  return <form onSubmit={/* ... */}>{/* email/password fields */}</form>
}
Note

A few things worth knowing about this pattern:

  • No token in client state, on purpose. credentials: 'include' sends whatever cookie set on login; the JWT itself never touches localStorage or React state, which closes off a whole class of XSS-driven token theft.
  • This assumes the cookie is httpOnly and secure. That's API's default for its auth cookie, but if you've customized cookie options on the backend, double-check they're still set — an httpOnly: false cookie defeats the point of this pattern.
  • Revalidation only happens on mount. The /api/users/me check runs once when AuthProvider mounts (e.g. app load), so if the server-side session expires mid-session, the client won't know until the next full mount or a request comes back unauthorized. If you need faster detection, that's the place to add a periodic check or a 401 interceptor.
  • CORS matters here. credentials: 'include' only works if the API's CORS config explicitly allows credentials and doesn't use a wildcard Access-Control-Allow-Origin.
© 2026 Sabin Shrestha