Building a Dynamic Navbar with Next.js 14 and GitHub OAuth
A Guide to Blending Server-Side Rendering (SSR) and Client-Side Rendering (CSR) with Next Auth
This is the navbar that we'll be creating today. Here's how it's designed:
Always display the logo on the left
Display the 'Create Post' button towards the right. This route will be protected by middleware and accessible to only authenticated users.
The right-most section will be dynamic based on whether there is a user session or not.
- In case of no user session, the 'Login with Github' button will be displayed.
- In case of an active user session, the profile picture of the user shall be displayed. Clicking on the picture should open up a menu which displays the user's name and a few other relevant routes in the application.
Prerequisites:
A Next.js project setup using the App router
NextAuth configuration for Github OAuth ( I referred to this tutorial to get this set up)
We begin by creating the Navbar
component and adding the static elements to it:
//app/ui/navbar.tsx
import Link from "next/link";
import Image from "next/image";
export default async function NavBar() {
return (
<nav className="flex items-center justify-between py-4 px-16 bg-white fixed w-full">
<Link href="/">
<Image src="/logo-color.png" width={50} height={60} alt="Logo" />
</Link>
<div className="flex items-center justify-center gap-4">
<Link
href="/new"
className="rounded-md bg-indigo-600 px-4 py-3.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Create Post
</Link>
</div>
</nav>
);
}
You should add an image to display your app's logo in the /public
folder and set the Image src
equal to the file name for the above code to work.
Now, let's dive into creating the dynamic components of the navbar: the Login button and the Navbar menu.
Here's how I created the Login button:
//app/ui/login-button.tsx
"use client";
import { signIn } from "next-auth/react";
const LoginButton = () => {
return (
<button
className="bg-slate-600 px-4 py-3.5 text-sm font-semibold shadow-sm text-white hover:bg-slate-500 rounded-md flex items-center justify-center gap-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600"
onClick={() => signIn("github", { callbackUrl: "/" })}
type="button"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.166 6.839 9.489.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.155-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.111-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.026A9.564 9.564 0 0112 6.838c.85.004 1.705.115 2.504.337 1.909-1.295 2.747-1.026 2.747-1.026.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.841-2.337 4.687-4.564 4.934.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .269.18.579.688.481A10.001 10.001 0 0022 12c0-5.523-4.477-10-10-10z"
clipRule="evenodd"
/>
</svg>
Login With GitHub
</button>
);
};
export default LoginButton;
I added "use client"
on the top of this file to make this a client component since we're using the onClick
event handler. I also specified the callbackUrl
to tell NextAuth where to redirect the user once they are logged in.
The <svg>
is used to create the Github logo on the button.
Next, we will write the code for the Navbar menu that is only visible once the user logs in. This will also be a client component as the menu will toggle based on user interaction.
"use client";
import { useState, useRef, useEffect } from "react";
import { FaUser } from "react-icons/fa";
import Link from "next/link";
interface User {
name?: string | null;
email?: string | null;
image?: string | null;
}
interface NavBarMenuProps {
user: User;
}
export default function NavBarMenu({ user }: NavBarMenuProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handler = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setIsMenuOpen(false);
console.log(menuRef.current);
}
};
document.addEventListener("mousedown", handler);
return () => {
document.removeEventListener("mousedown", handler);
};
});
const menuItems = [
{ name: "Dashboard", href: "/dashboard" },
{ name: "Bookmarks", href: "/bookmarks" },
{ name: "Sign out", href: "/signout" },
];
const imageDisplay = user?.image ? (
<img
src={user?.image}
alt="Profile pic"
className="h-10 w-10 rounded-full border-2 border-gray-600 focus:border-white"
/>
) : (
<div className="h-10 w-10 rounded-full border-2 border-gray-600 focus:border-white flex items-center justify-center">
<FaUser className="text-xl" />
</div>
);
return (
<div className="relative" ref={menuRef}>
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="flex items-center justify-center"
>
{imageDisplay}
</button>
{isMenuOpen && (
<div className="absolute right-0 mt-2 py-2 w-48 bg-white rounded-md shadow-xl z-20">
<div className="block px-4 py-2 text-sm text-gray-700 border-b">
{user?.name}
</div>
{menuItems.map((item) => (
<Link
key={item.name}
href={item.href}
className="block px-4 py-2 text-sm text-gray-700 hover:bg-indigo-100 hover:underline"
>
{item.name}
</Link>
))}
</div>
)}
</div>
);
}
In the above code, first a button is rendered that will display the user's Github profile picture if it's available, otherwise it will display a user icon imported from the react-icons/fa
library. Clicking on the button toggles the isMenuOpen
state. We conditionally render the user's name and all the menu items if the isMenuOpen
state is set to true
.
I also added additonal code to close the menu if the user clicks anywhere else on the screen other than the menu div
to make it a smoother experience.
Now, the only thing remaining is to conditionally render the Login or the Menu button based on user session. We can add these lines to the navbar
component to achieve this:
//app/ui/navbar.tsx
//other imports
import { getServerSession } from "next-auth/next";
import { options } from "@/app/api/auth/[...nextauth]/options";
import LoginButton from "./login-button";
import NavBarMenu from "./navbar-menu";
export default async function NavBar() {
const session = await getServerSession(options);
return (
<nav className="flex items-center justify-between py-4 px-16 bg-white fixed w-full">
// ...
<div className="flex items-center justify-center gap-4">
// ...
{!session ? (
<LoginButton />
) : (
<>{session?.user && <NavBarMenu user={session.user} />}</>
)}
</div>
</nav>
);
}
We could have made the navbar
component a client component as well but since it requires session data, it would mean shipping extra code to the client which isn't the most optimal way of doing things.
Note that since the navbar
is a server component, any changes you make to its child components might not be visible on hot reload and you might have to do a build for the changes to take place on the browser while developing locally.
And that's it, your beautiful dynamic Navbar is ready! ๐