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.
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.
'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 #
import { AuthProvider } from '@/providers/AuthContext'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
)
}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>
}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 toucheslocalStorageor React state, which closes off a whole class of XSS-driven token theft. - This assumes the cookie is
httpOnlyandsecure. 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 — anhttpOnly: falsecookie defeats the point of this pattern. - Revalidation only happens on mount. The
/api/users/mecheck runs once whenAuthProvidermounts (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 wildcardAccess-Control-Allow-Origin.
Related content
A mental model for Server vs Client Components
The rule of thumb I actually use when deciding whether a component needs 'use client', and how a Server Component parent can still pass data down to it.
Route Handlers can return a plain Response, not just NextResponse
TIL you don't need NextResponse for a simple Route Handler — a standard Web API Response works fine, which matters for things like hand-rolled RSS/XML endpoints.
Clonify.io: Turning a Slow WordPress Marketplace into a High-Performance Next.js SaaS Platform
Rebuilding a digital-products marketplace from WordPress to Next.js and Payload CMS, with a custom JSON caching layer and secure, subscription-aware asset delivery.