Global User State
Used in this guide:Requirement:
1. Install TanStack Reach Query
npm install @tanstack/react-query2. Create auth functions
SSR Version
This version will be used in main layout.tsx to prefetch and cache user data since we don't want to make the whole site CSR.
import { headers } from "next/headers";
import { getRootUrl } from "../utility";
import { User } from "@/payload-types";
export async function getUser(): Promise<User | null> {
try {
const cookie = (await headers()).get('cookie');
if (!cookie) {
return null;
}
const rootUrl = getRootUrl();
const res = await fetch(`${rootUrl}/api/users/me`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cookie": cookie,
},
});
if (!res.ok) {
throw new Error(`Server responses with ${res.status}`);
}
const data = await res.json();
if (data.user) {
return data.user;
} else {
throw new Error("Can't find user object or invalid data format");
}
} catch (error) {
console.error("Error getting the user: ", (error as Error).message);
return null
}
}CSR Version
We will use this version when we create useUser hook with TanStack useQuery. This version uses credentials instead of cookie and it works with CSR components.
import { getRootUrl } from "../utility";
export async function getUserClient() {
try {
const rootUrl = getRootUrl();
const res = await fetch(`${rootUrl}/api/users/me`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
});
if (!res.ok) {
return null;
}
const data = await res.json();
return data.user || null;
} catch (error) {
console.error("Error getting the user on client: ", (error as Error).message);
return null;
}
}3. Create useUser Hook
This hook is used to run the getUserClient function we created above. The benefits of using TanStack for this is that you can track states and cache data.
'use client';
import { getUserClient } from "@/lib/payload/authClient";
import { useQuery } from "@tanstack/react-query";
export const useUser = () => {
// call(run) getUser function
// track the following states: loading, error, and data
// cache the result with the key 'user'
// benefits: reuse cached data instantly, avoid duplicate requests, automatically refetch in the background (optional)
const { data, isLoading, isError, error } = useQuery({
queryKey: ['user'],
queryFn: getUserClient,
});
return { data, isLoading, isError, error };
}4. Create QueryProvider
'use client'
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ReactNode, useState } from "react"
export default function QueryProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}5. Put QueryProvider in the Main Layout
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Navbar from "./components/Navbar";
import QueryProvider from "./components/QueryProvider";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "N | Payload Auth",
description: "Generated by create next app",
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased
w-full max-w-[1280px] p-8 mx-auto`}
>
<QueryProvider>
<Navbar />
{children}
</QueryProvider>
</body>
</html>
);
}6. Create UserCard component
With our current setup, we are ready to call useUser hook anywhere in any client components of our app. So let's create a user card to display user info.
This example includes logout logic that point directly to built-in Payload api that we don't have to create.
'use client'
import { useUser } from "@/hooks/useUser"
import { useQueryClient } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
const UserCard = () => {
const { data: user, isLoading } = useUser();
const router = useRouter();
const queryClient = useQueryClient();
const handleLogout = async () => {
try {
const res = await fetch(`api/users/logout`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
credentials: "include",
});
if (!res.ok) {
throw new Error(`Server responded with status: ${res.status} ${res.statusText}`)
}
queryClient.removeQueries({ queryKey: ['user'] });
router.push("/login");
} catch (error) {
console.error("An error occurred. ", (error as Error).message);
}
}
return (
<div className="rounded-lg bg-gray-700 p-10 w-[300px]">
<p>Name: {isLoading ? "Loading..." : (user?.profile.firstName ?? user?.name) || "Guest"}</p>
{user && (
<button
onClick={handleLogout}
className="py-2 px-4 text-sm bg-amber-600 mt-2 cursor-pointer"
>Logout</button>
)}
</div>
)
}
export default UserCard;7. Update Homepage
We will use our UserCard here instead.
import UserCard from "./components/UserCard";
export default function Home() {
return (
<div>
<h1>Test Payload Auth</h1>
<UserCard />
</div>
);
}At this point, you should start the server and test how things look. If you're logged-in, you should see your name displayed on the card.
8. Create Updatable Profile Page
'use client'
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';
import { useUser } from '@/hooks/useUser';
import { getRootUrl } from '@/lib/utility';
export default function ShippingAddressPage() {
const router = useRouter();
const { data: user, isLoading, error } = useUser();
const queryClient = useQueryClient();
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
address1: '',
address2: '',
city: '',
state: '',
zipcode: '',
country: '',
});
const [status, setStatus] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
// Populate the form with user data once it's loaded
useEffect(() => {
if (user && user.profile) { // Add a check for user.profile
setFormData({
firstName: user.profile.firstName ?? user.name ?? '',
lastName: user.profile.lastName || '',
address1: user.profile.address1 || '',
address2: user.profile.address2 || '',
city: user.profile.city || '',
state: user.profile.state || '',
zipcode: user.profile.zipcode || '',
country: user.profile.country || '',
});
}
}, [user]);
// Handle form input changes
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prevData => ({
...prevData,
[name]: value,
}));
};
// Handle form submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setStatus('');
if (!user || !user.profile) { // Add a check for user.profile
setStatus('You must be logged in to update your address.');
setIsSubmitting(false);
return;
}
try {
const rootUrl = getRootUrl();
const profileId = user.profile.id; // Get the profileId here, after the user check
const res = await fetch(`${rootUrl}/api/profiles/${profileId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(formData),
});
if (res.ok) {
await queryClient.invalidateQueries({ queryKey: ['user'] });
setStatus('Profile updated successfully!');
} else {
const errorData = await res.json();
setStatus(errorData.message || 'Failed to update profile.');
}
} catch (err) {
console.error('Update address error:', err);
setStatus('An unexpected error occurred. Please try again.');
} finally {
setIsSubmitting(false);
}
};
if (isLoading) {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24 bg-gray-100">
<p>Loading user data...</p>
</main>
);
}
if (error) {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24 bg-gray-100">
<p className="text-red-500">Error: Failed to load user data.</p>
</main>
);
}
if (!user) {
router.push('/login');
return null;
}
return (
<main className="flex flex-col">
<div className="bg-gray-600 p-8 rounded-lg shadow-md w-full max-w-md">
<h1 className="text-3xl font-bold mb-6 text-center">Update profile</h1>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Input fields for all address components */}
<input
type="text"
name="firstName"
placeholder="First Name"
value={formData.firstName}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
<input
type="text"
name="lastName"
placeholder="Last Name"
value={formData.lastName}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
<input
type="text"
name="address1"
placeholder="Address Line 1"
value={formData.address1}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
<input
type="text"
name="address2"
placeholder="Address Line 2"
value={formData.address2}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
<input
type="text"
name="city"
placeholder="City"
value={formData.city}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
<input
type="text"
name="state"
placeholder="State/Province/Region"
value={formData.state}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
<input
type="text"
name="zipcode"
placeholder="Zip/Postal Code"
value={formData.zipcode}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
<input
type="text"
name="country"
placeholder="Country"
value={formData.country}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
{status && (
<p className={`text-sm text-center ${status.includes('success') ? 'text-green-500' : 'text-red-500'}`}>
{status}
</p>
)}
<button
type="submit"
disabled={isSubmitting}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-emerald-600 hover:bg-emerald-700 disabled:opacity-50"
>
{isSubmitting ? "Loading..." : 'Update Address'}
</button>
</form>
</div>
</main>
);
}At this point, you should be able to view and update your profile info freely in the profile page.
Next, you should go back to Droplet and Setup pgBouncer
And if it's not your first time, go straight to Give pgBouncer a list of users