I’m new to the Next.js framework and have the issue described in the title.
app/Layout.tsx
import '@/app/ui/globals.css';
import { inter } from '@/app/ui/fonts';
import AppBar from '@/app/ui/app-bar';
import { getTickers } from '@/app/lib/tickers';
import { auth } from '@/auth';
import { SessionProvider } from 'next-auth/react';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const tickers = await getTickers();
const session = await auth();
return (
<html lang="en">
<body className={`${inter.className} antialiased`}>
<SessionProvider session={session}>
<AppBar options={tickers} />
{children}
</SessionProvider>
</body>
</html>
);
}
app/ui/app-bar.tsx <- The ‘use client’ component that doesn’t update.
'use client'
import { useState, useEffect, useRef } from "react";
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { signOut, useSession } from 'next-auth/react';
export default function AppBar({ options }: { options: { label: string, value: string }[] }) {
const { data: session } = useSession();
const user = session?.user;
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<{ label: string, value: string }[]>([]);
const router = useRouter();
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsUserMenuOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const term = e.target.value.toLowerCase();
setSearchTerm(term);
if (term.length > 0) {
const filtered = options.filter(option =>
option.label.toLowerCase().includes(term) ||
option.value.toLowerCase().includes(term)
);
setSearchResults(filtered);
} else {
setSearchResults([]);
}
};
const handleSelectOption = (value: string) => {
setSearchTerm('');
setSearchResults([]);
router.push(`/companies/${value}`);
};
const getInitials = (firstName: string | null | undefined, lastName: string | null | undefined) => {
const first = firstName ? firstName[0] : '';
const last = lastName ? lastName[0] : '';
return (first + last).toUpperCase();
};
const handleLogout = async () => {
await signOut({ redirect: false });
window.location.href = '/login'; // <- Forcing a hard refresh here to update the app bar
};
const userInitials = user ? getInitials(user.first_name, user.last_name) : 'G'; // G for Guest User
return (
<nav className="bg-blue-600 p-4">
<div className="container mx-auto flex items-center justify-between">
<Link href="/" className="text-white text-xl font-bold">
Company Name
</Link>
<div className="flex-grow mx-4 relative">
<input
type="text"
placeholder="Ticker or company name"
className="w-full p-2 bg-blue-500 text-white placeholder-blue-200 border border-blue-400 rounded focus:outline-none focus:ring-2 focus:ring-blue-300 focus:border-blue-400"
value={searchTerm}
onChange={handleSearch}
/>
{searchResults.length > 0 && (
<ul className="absolute z-10 w-full bg-white mt-1 rounded-md shadow-lg max-h-60 overflow-auto">
{searchResults.map((result) => (
<li
key={result.value}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
onClick={() => handleSelectOption(result.value)}
>
{result.label} ({result.value})
</li>
))}
</ul>
)}
</div>
<div className="relative" ref={menuRef}>
{user ? (
<div className="relative ml-3">
<div>
<button
type="button"
className="flex rounded-full bg-blue-700 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-blue-600"
id="user-menu-button"
aria-expanded="false"
aria-haspopup="true"
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
>
<span className="sr-only">Open user menu</span>
<div className="h-10 w-10 rounded-full bg-white flex items-center justify-center text-blue-600 font-bold text-lg shadow-lg border-2 border-blue-300 transition-all duration-200 hover:scale-110">
{userInitials}
</div>
</button>
</div>
{isUserMenuOpen && (
<div className="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabIndex={-1}>
<div className="px-4 py-2 text-sm text-gray-500 border-b border-gray-200">
{user?.first_name} {user?.last_name}
</div>
<Link href="/settings/account" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem" tabIndex={-1}>
Account
</Link>
<Link href="/settings/security" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem" tabIndex={-1}>
Password & Security
</Link>
<Link href="/settings/billing" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem" tabIndex={-1}>
Subscription & Billing
</Link>
<div className="border-t border-gray-200 my-1"></div>
<button
onClick={handleLogout}
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
role="menuitem"
tabIndex={-1}
>
Sign Out
</button>
</div>
)}
</div>
) : (
<Link href="/login" className="text-white hover:bg-blue-700 px-3 py-2 rounded-md text-sm font-medium">
Sign In
</Link>
)}
</div>
</div>
</nav>
);
}
auth.ts
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { authConfig } from './auth.config';
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import type { User } from '@/app/lib/definitions';
import bcrypt from 'bcrypt';
async function getUser(email: string): Promise<User | undefined> {
try {
const user = await sql<User>`SELECT * FROM users WHERE email=${email}`;
return user.rows[0];
} catch (error) {
console.error('Failed to fetch user:', error);
throw new Error('Failed to fetch user.');
}
}
export const { handlers, auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [
Credentials({
async authorize(credentials) {
const parsedCredentials = z
.object({ email: z.string().email(), password: z.string().min(6) })
.safeParse(credentials);
if (parsedCredentials.success) {
const { email, password } = parsedCredentials.data;
const user = await getUser(email);
if (!user) return null;
if (!user.email_verified) {
throw new Error('Please verify your email before logging in.');
}
const passwordsMatch = await bcrypt.compare(password, user.password);
if (passwordsMatch) {
return {
id: user.id,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
};
}
}
console.log('Invalid credentials');
return null;
},
}),
],
callbacks: {
async jwt({ token, user }) {
//console.log('auth.ts user, token', user, token);
if (user) {
token.first_name = user.first_name;
token.last_name = user.last_name;
}
//console.log('auth.ts changed token', token);
return token;
},
async session({ session, token }) {
//console.log('auth.ts session, token', session, token);
if (session.user) {
session.user.first_name = token.first_name as string;
session.user.last_name = token.last_name as string;
}
//console.log('auth.ts changed session', session);
return session;
},
}
});
auth.config.ts <- Where the redirect happens
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnLoginPage = nextUrl.pathname.startsWith('/login')
const isOnRegisterPage = nextUrl.pathname.startsWith('/register')
if (isLoggedIn) {
if (isOnLoginPage || isOnRegisterPage) {
return Response.redirect(new URL('/', nextUrl));
}
}
return true;
},
async jwt({ token, user }) {
if (user) {
token.first_name = user.first_name;
token.last_name = user.last_name;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.first_name = token.first_name as string;
session.user.last_name = token.last_name as string;
}
return session;
},
},
providers: [],
} satisfies NextAuthConfig;
next-auth.d.ts
import NextAuth from 'next-auth';
declare module 'next-auth' {
interface Session {
user: {
first_name: string;
last_name: string;
} & DefaultSession['user'];
}
interface User {
first_name: string;
last_name: string;
}
}
declare module "next-auth/jwt" {
/** Returned by the `jwt` callback and `getToken`, when using JWT sessions */
interface JWT {
first_name: string;
last_name: string;
}
}
app/api/[…nextauth]/route.ts
import { handlers } from "@/auth"
export const { GET, POST } = handlers
middleware.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export default NextAuth(authConfig).auth;
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.*\.png$).*)'],
};
app/login/page.tsx
import LoginForm from "@/app/ui/login-form";
export default function LoginPage() {
return (
<main>
<LoginForm />
</main>
);
}
app/ui/login-form.tsx
'use client';
import Link from "next/link";
import { useActionState } from "react";
import { authenticate } from "../lib/actions";
export default function LoginForm() {
const [errorMessage, formAction, isPending] = useActionState(
authenticate,
undefined,
);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<div className="mx-auto h-12 w-12 rounded-full bg-blue-500 flex items-center justify-center">
<svg className="h-6 w-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in
</h2>
</div>
<form action={formAction} className="mt-8 space-y-6">
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">Email address</label>
<input id="email" name="email" type="email" autoComplete="email" required className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" placeholder="Email address" />
</div>
<div>
<label htmlFor="password" className="sr-only">Password</label>
<input id="password" name="password" type="password" autoComplete="current-password" required className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" placeholder="Password" />
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<Link href="#" className="font-medium text-blue-600 hover:text-blue-500">
Forgot your password?
</Link>
</div>
</div>
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
disabled={isPending}>
{isPending ? 'Signing In...' : 'Sign In'}
</button>
</div>
{errorMessage && (
<p className="mt-2 text-center text-sm text-red-600">
{errorMessage}
</p>
)}
</form>
<div className="text-sm text-center">
<Link href="/register" className="font-medium text-blue-600 hover:text-blue-500">
Don't have an account? Sign Up
</Link>
</div>
</div>
</div>
);
}
I expected the AppBar to update automatically when user’s are authenticated.
I am using Next.js 15.0.0-canary.106 with react & react-dom 19.0.0-rc-06d0b89e-20240801.
Can anyone point me as to why the AppBar doesn’t update when user logs in successfully?