I just told you… STOP FORGETTING!!
Don’t you hate it when that AI app keeps forgetting the things you’ve told it?!? In this tutorial, we'll solve this problem by building a Next.js chat application with MongoDB as the memory provider for the Vercel AI SDK.
What you'll learn
By the end of this tutorial, you'll have:
- A fully functional Next.js app using the App Router.
- AI chat capabilities with the Vercel AI SDK.
- Persistent chat history using MongoDB.
- A clean UI with chat history navigation.
- Dark/light mode theme support with shadcn/ui components.
The best part? This entire setup can run on MongoDB Atlas's free tier, making it perfect for prototypes, side projects, or learning experiences.
What is the Vercel AI SDK?
The Vercel AI SDK is a library that simplifies building AI-powered user interfaces. It provides a set of tools and abstractions that make it easy to integrate AI models into your applications with minimal boilerplate code. The SDK handles streaming responses, managing state, and provides hooks for React applications that work seamlessly with various AI providers like OpenAI, Anthropic, and others.
What makes the Vercel AI SDK particularly valuable is its developer experience (DX). It offers type-safe APIs, streaming capabilities out of the box, and a unified interface across different AI providers. This means you can switch between models or providers without rewriting your application logic. The SDK also includes built-in support for features like rate limiting, error handling, and now—with memory providers like the one we're building—conversation history persistence.
Prerequisites
Before we jump in, make sure you have:
- Node.js 18.18 or later installed.
- A MongoDB Atlas account (the free tier works perfectly).
- Basic knowledge of Next.js and React.
- Some familiarity with TypeScript.
- An OpenAI API key.
Want to cheat? Peek the complete code example. 😅
Project setup
Let's build this application step by step. We'll start with setting up our project and gradually add features until we have a fully functional chat application with persistent memory.
Create a new Next.js project
First, let's create a fresh Next.js project using the create-next-app CLI:
npx create-next-app@latest chat-with-memory
When prompted, select the following options:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- App Router: Yes
- Use
src
directory: Yes - Import alias: Yes (default
@/*
)
Install dependencies
Navigate to your project directory and install the required dependencies:
cd chat-with-memory
npm install ai @ai-sdk/react @ai-sdk/openai mongodb
Set up shadcn/ui
We'll use shadcn/ui for our components. First, initialize shadcn/ui in your project:
npx shadcn@latest init
Now, let's add the specific UI components we'll need:
npx shadcn@latest add button input card avatar scroll-area separator dropdown-menu
Configure environment variables
Create a .env.local
file in your project root. This will store our MongoDB and OpenAI configuration:
MONGODB_URI=your_mongodb_uri_here
MONGODB_DB=chat_history_db
OPENAI_API_KEY=your_openai_api_key_here
Make sure to replace the placeholder values with your actual MongoDB URI and OpenAI API key. If you don't have these yet:
- Get your MongoDB URI from MongoDB Atlas by creating a free cluster.
- Get your OpenAI API key from the OpenAI dashboard.
Now that we have our project set up with all the necessary dependencies and configuration, we can start building the core functionality of our chat application.
Setting up MongoDB connection
Create MongoDB connection utility
First, let's create a utility to handle our MongoDB connections. This is a crucial part of our application as it ensures we maintain efficient database connections, especially in a serverless environment.
Create a new directory called lib
in your project's src
directory, then create a file called mongodb.ts
inside it:
import { MongoClient } from 'mongodb';
if (!process.env.MONGODB_URI) {
throw new Error('Please define the MONGODB_URI environment variable');
}
if (!process.env.MONGODB_DB) {
throw new Error('Please define the MONGODB_DB environment variable');
}
const uri = process.env.MONGODB_URI;
let client: MongoClient;
let clientPromise: Promise<MongoClient>;
// In development, we use a global variable to maintain the connection
// This prevents creating new connections during hot reloads
if (process.env.NODE_ENV === 'development') {
const globalWithMongo = global as typeof global & {
_mongoClientPromise?: Promise<MongoClient>;
};
if (!globalWithMongo._mongoClientPromise) {
client = new MongoClient(uri);
globalWithMongo._mongoClientPromise = client.connect();
}
clientPromise = globalWithMongo._mongoClientPromise;
} else {
// In production, create a new connection for each instance
client = new MongoClient(uri);
clientPromise = client.connect();
}
export default clientPromise;
This connection utility employs a critical pattern for serverless environments. In development mode, we attach the MongoDB client promise to the global object, ensuring that hot reloads (which happen frequently during development) don't create multiple connections. In production, we create a new connection for each server instance, but thanks to Next.js's module caching, this still maintains a single connection per instance rather than per request.
Now that we have our MongoDB connection utility set up, we can start building the data layer for our chat functionality.
Building the chat data layer
Create chat server actions
Let's create server actions to interact with MongoDB for storing and retrieving chat messages. These actions will provide a clean API for our client components to perform database operations.
Create an actions
folder inside your app
directory, then create a file called chat-actions.ts
. Let's build this file step by step:
First, let's add our imports and define our types:
'use server'
import { Message } from 'ai';
import { ObjectId } from 'mongodb';
import clientPromise from '@/lib/mongodb';
export interface ChatMemory {
_id: ObjectId;
messages: Message[];
createdAt: Date;
updatedAt: Date;
}
Now, let's add the function to create new chats:
/**
* Creates a new chat in MongoDB
*/
export async function createChat() {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const collection = db.collection<ChatMemory>('chats');
const now = new Date();
const result = await collection.insertOne({
_id: new ObjectId(),
messages: [],
createdAt: now,
updatedAt: now
});
return result.insertedId.toString();
}
Next, add the function to retrieve a specific chat:
/**
* Gets a chat by ID from MongoDB
*/
export async function getChat(chatId: string) {
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const collection = db.collection<ChatMemory>('chats');
const objectId = new ObjectId(chatId);
const chat = await collection.findOne({ _id: objectId });
if (!chat) {
return null;
}
return {
id: chat._id.toString(),
messages: chat.messages,
createdAt: chat.createdAt,
updatedAt: chat.updatedAt
};
} catch (error) {
console.error("Error fetching chat:", error);
return null;
}
}
Add the function to save messages to a chat:
/**
* Saves messages to a chat in MongoDB
*/
export async function saveMessages(chatId: string, messages: Message[]) {
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const collection = db.collection<ChatMemory>('chats');
const objectId = new ObjectId(chatId);
await collection.updateOne(
{ _id: objectId },
{
$set: {
messages: messages,
updatedAt: new Date()
}
},
{ upsert: true }
);
return true;
} catch (error) {
console.error("Error saving chat:", error);
return false;
}
}
Now, let's add the function to retrieve all chats:
/**
* Gets all chats from MongoDB
*/
export async function getAllChats(limit: number = 10) {
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const collection = db.collection<ChatMemory>('chats');
const chats = await collection
.find({
// Only return chats that have at least one message
"messages.0": { $exists: true }
})
.sort({ updatedAt: -1 })
.limit(limit)
.toArray();
return chats.map((chat: ChatMemory) => ({
id: chat._id.toString(),
messages: chat.messages,
createdAt: chat.createdAt,
updatedAt: chat.updatedAt
}));
} catch (error) {
console.error("Error fetching all chats:", error);
return [];
}
}
Finally, add the function to delete chats:
/**
* Deletes a chat from MongoDB
*/
export async function deleteChat(chatId: string) {
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const collection = db.collection<ChatMemory>('chats');
const objectId = new ObjectId(chatId);
const result = await collection.deleteOne({ _id: objectId });
return result.deletedCount === 1;
} catch (error) {
console.error("Error deleting chat:", error);
return false;
}
}
These server actions provide five key operations:
-
createChat
: Creates a new empty chat document with timestamps -
getChat
: Retrieves a specific chat by ID -
saveMessages
: Updates a chat with new messages -
getAllChats
: Fetches a list of chats with at least one message -
deleteChat
: Removes a chat from the database
Each function follows best practices for error handling and type safety. The 'use server'
directive at the top indicates these are server actions in Next.js, meaning they run on the server but can be imported and called directly from client components.
Creating the chat API route
Set up the chat API endpoint
Now, we'll create the API route that handles communication between our chat UI and the AI model while persisting conversations to MongoDB.
Create a new file at app/api/chat/route.ts
:
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { getChat, createChat, saveMessages } from '@/app/actions/chat-actions';
export async function POST(req: Request) {
const { messages, chatId } = await req.json();
// Get existing chat or create a new one
let currentChatId = chatId;
if (!currentChatId) {
currentChatId = await createChat();
}
// Get existing messages if any
const existingChat = await getChat(currentChatId);
const existingMessages = existingChat?.messages || [];
// Use latest messages from the client but verify with server-side history
const messagesForModel = messages.length > 0 ? messages : existingMessages;
// Generate response from the AI model
const result = streamText({
model: openai('gpt-4o-mini'),
messages: messagesForModel,
temperature: 0.7,
maxTokens: 1000,
// When the stream is complete, save all messages including the new response
onFinish: async (response) => {
const updatedMessages = [
...messagesForModel,
{ role: 'assistant', content: response.text, id: Date.now().toString() }
];
// Save to MongoDB
await saveMessages(currentChatId, updatedMessages);
}
});
// Return streaming response with chatId in headers
return result.toDataStreamResponse({
headers: {
'Content-Type': 'text/event-stream',
'X-Chat-Id': currentChatId,
},
});
}
This API route serves as the bridge between our UI and the AI model, handling several important tasks:
- Chat session management:
- Determines if we're continuing an existing chat or starting a new one
- Creates a MongoDB document for new chats
- Returns the chat ID to the client
- Message history verification:
- Verifies the client's message history against the server-side history
- Ensures data consistency even if the client state is outdated
- AI response streaming:
- Uses the Vercel AI SDK's
streamText
function for streaming responses - Creates a better user experience with incremental responses
- Saves both original messages and AI responses to MongoDB
- Uses the Vercel AI SDK's
- Custom headers:
- Returns the chat ID in a custom header
- Allows the client to update its state for future requests
The route uses a pattern that balances client and server state. While the client maintains the current conversation state for immediate UI updates, the server remains the source of truth for persistent storage.
Building the UI components
Create the main Chat component
Now, let's build the UI for our chat application, starting with the main chat component. In the components
folder in your project src
directory, add a file called Chat.tsx
.
First, let's add our imports and component interface:
'use client'
import { useChat } from "@ai-sdk/react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { User, Bot } from "lucide-react";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { getChat } from "@/app/actions/chat-actions";
import { Message } from "ai";
export default function Chat({
chatId,
onChatCreated,
onMessageSent
}: {
chatId?: string;
onChatCreated?: () => void;
onMessageSent?: () => void;
}) {
Next, let's add our state management and chat history loading:
const router = useRouter();
const [initialMessages, setInitialMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [chatKey, setChatKey] = useState(Date.now());
// Fetch chat history when chatId changes
useEffect(() => {
async function loadChatHistory() {
if (chatId) {
setIsLoading(true);
const chat = await getChat(chatId);
if (chat && chat.messages) {
setInitialMessages(chat.messages);
}
setIsLoading(false);
// Generate a new key to force the useChat hook to reset
setChatKey(Date.now());
} else {
setInitialMessages([]);
setChatKey(Date.now());
}
}
loadChatHistory();
}, [chatId]);
Now, let's set up the chat hook and handle chat creation:
const {
messages,
input,
handleInputChange,
handleSubmit,
status,
error
} = useChat({
api: "/api/chat",
id: chatId,
initialMessages,
key: chatKey.toString(), // Use a key to force reset when chatId changes
body: {
chatId: chatId,
},
onResponse: (response) => {
// If a new chat was created, update the URL
const newChatId = response.headers.get('X-Chat-Id');
if (newChatId && !chatId) {
// Force a refresh to update the sidebar
router.refresh();
// Trigger sidebar refresh via callback
onChatCreated?.();
}
},
onFinish: () => {
// When a message exchange is complete, refresh the sidebar
onMessageSent?.();
}
});
// Get title from the first user message (up to 20 characters)
const title = messages.find(m => m.role === "user")?.content?.substring(0, 20) || "New conversation";
Finally, let's add the component's JSX:
return (
<Card className="w-full max-w-2xl mx-auto shadow-md">
<CardHeader>
<CardTitle className="text-xl">{chatId ? `${title}...` : "New chat"}</CardTitle>
</CardHeader>
<Separator />
<CardContent className="p-0">
<ScrollArea className="h-[400px] p-4">
{isLoading ? (
<div className="text-center py-8 text-muted-foreground">
Loading chat history...
</div>
) : messages.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No messages yet. Start a conversation!
</div>
) : (
messages.map((m, index) => (
<div
key={index}
className={cn(
"flex items-start gap-3 mb-4",
m.role === "user" ? "justify-end" : "justify-start"
)}
>
{m.role !== "user" && (
<Avatar className="h-8 w-8 bg-secondary">
<AvatarFallback className="flex items-center justify-center">
<Bot className="h-4 w-4 text-secondary-foreground" />
</AvatarFallback>
</Avatar>
)}
<div
className={cn(
"rounded-lg px-3 py-2 max-w-[80%]",
m.role === "user"
? "bg-primary text-primary-foreground"
: "bg-muted text-foreground"
)}
>
{m.content}
</div>
{m.role === "user" && (
<Avatar className="h-8 w-8 bg-secondary">
<AvatarFallback className="flex items-center justify-center">
<User className="h-4 w-4 text-secondary-foreground" />
</AvatarFallback>
</Avatar>
)}
</div>
))
)}
{(status === "streaming" || status === "submitted") && (
<div className="flex items-center gap-2 text-muted-foreground ml-11 mt-2">
<div className="flex space-x-1">
<div className="animate-bounce h-2 w-2 rounded-full bg-current"></div>
<div className="animate-bounce h-2 w-2 rounded-full bg-current delay-100"></div>
<div className="animate-bounce h-2 w-2 rounded-full bg-current delay-200"></div>
</div>
<span>AI is thinking...</span>
</div>
)}
</ScrollArea>
</CardContent>
<Separator />
<CardFooter className="p-4">
{error && <div className="text-destructive mb-2 text-sm">{error.message}</div>}
<form onSubmit={handleSubmit} className="flex w-full gap-2">
<Input
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
disabled={status === "streaming" || status === "submitted" || isLoading}
className="flex-1"
/>
<Button
type="submit"
disabled={(status === "streaming" || status === "submitted") || !input.trim() || isLoading}
>
Send
</Button>
</form>
</CardFooter>
</Card>
);
}
The Chat component integrates several key features:
- Dynamic history loading:
- Loads chat history when the chatId changes
- Shows loading states during data fetching
-
useChat
hook integration:- Handles message state and input management
- Connects to our API route
- Manages streaming responses
- Chat state management:
- Uses a unique key to reset the chat when needed
- Handles new chat creation and updates
- UI features:
- Distinct styling for user and AI messages
- Loading indicators for AI responses
- Error handling and display
- Responsive message layout
The component uses shadcn/ui components to create a polished interface with minimal effort. The ScrollArea component ensures the chat remains scrollable as it fills with messages.
Create the chat sidebar component
Now, let's create the component that displays our chat history. Add a file called ChatSidebar.tsx
in the components
directory.
First, let's add our imports and types:
'use client'
import { useEffect, useState } from "react";
import { getAllChats, deleteChat } from "@/app/actions/chat-actions";
import { Message } from "ai";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
interface Chat {
id: string;
messages: Message[];
createdAt: Date;
updatedAt: Date;
}
export default function ChatSidebar({
activeChatId,
onChatSelect,
refreshTrigger = 0,
onChatDeleted
}: {
activeChatId?: string;
onChatSelect: (chatId: string) => void;
refreshTrigger?: number;
onChatDeleted?: () => void;
}) {
Next, let's add our state management and chat loading logic:
const [chats, setChats] = useState<Chat[]>([]);
const [loading, setLoading] = useState(true);
// Load chats when component mounts or refreshTrigger changes
useEffect(() => {
async function loadChats() {
setLoading(true);
const chatsList = await getAllChats(20);
setChats(chatsList);
setLoading(false);
}
loadChats();
}, [refreshTrigger]);
// Get title from first user message (up to 20 characters)
const getChatTitle = (chat: Chat) => {
const firstUserMessage = chat.messages.find(m => m.role === "user");
if (firstUserMessage?.content) {
const title = firstUserMessage.content.substring(0, 20);
return title.length < firstUserMessage.content.length
? `${title}...`
: title;
}
return "New conversation";
};
Now, let's add the chat deletion handler:
const handleDeleteChat = async (e: React.MouseEvent, chatId: string) => {
e.stopPropagation();
await deleteChat(chatId);
setChats(chats.filter(chat => chat.id !== chatId));
// If the active chat was deleted, clear the selection
if (activeChatId === chatId) {
onChatSelect("");
}
// Notify parent component about the deletion
onChatDeleted?.();
};
Finally, let's add the component's JSX:
return (
<div className="w-64 border-r h-screen bg-muted/5">
<div className="p-4 border-b">
<h2 className="font-semibold">Your conversations</h2>
</div>
<ScrollArea className="h-[calc(100vh-65px)]">
<div className="p-2">
{loading ? (
<div className="text-center py-8 text-muted-foreground">Loading...</div>
) : chats.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">No conversations yet</div>
) : (
chats.map((chat) => (
<div
key={chat.id}
onClick={() => onChatSelect(chat.id)}
className={cn(
"flex items-center justify-between p-3 mb-1 rounded-md hover:bg-muted cursor-pointer group",
activeChatId === chat.id && "bg-muted"
)}
>
<div className="truncate flex-1">{getChatTitle(chat)}</div>
<Button
variant="ghost"
size="icon"
className="opacity-0 group-hover:opacity-100 h-7 w-7"
onClick={(e) => handleDeleteChat(e, chat.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))
)}
</div>
</ScrollArea>
</div>
);
}
The ChatSidebar component provides several key features:
- Dynamic chat loading:
- Fetches chats from MongoDB using
getAllChats
- Shows loading and empty states
- Refreshes when triggered by parent component
- Fetches chats from MongoDB using
- Chat selection:
- Highlights the active chat
- Notifies parent component when a chat is selected
- Chat deletion:
- Allows users to delete conversations
- Updates local state and notifies parent
- Handles active chat deletion gracefully
- UI features:
- Scrollable list of conversations
- Delete button appears on hover
- Truncated chat titles from first message
- Visual feedback for active chat
The sidebar uses the cn
utility function from shadcn/ui to conditionally apply Tailwind classes, making the selected chat visually distinct. The delete button only appears when hovering over a chat item, keeping the interface clean while still providing functionality.
Adding theme support
Create theme components
Let's add theme support to our application. First, create a theme-provider.tsx
file in the components directory:
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
Next, create a theme toggle component in theme-toggle.tsx
:
"use client"
import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function ThemeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
Update the root layout
Now, let's update the root layout to include theme support. Update app/layout.tsx
:
import type { Metadata } from "next";
import { Inter as FontSans } from "next/font/google";
import "./globals.css";
import { cn } from "@/lib/utils";
import { ThemeProvider } from "@/components/theme-provider";
const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});
export const metadata: Metadata = {
title: "AI Chat with MongoDB Memory",
description: "An AI chat application with persistent memory using MongoDB",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={cn(
"min-h-screen bg-background font-sans antialiased",
fontSans.variable
)}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}
Create the home page
Finally, let's create the main page that brings everything together. Update app/page.tsx
:
"use client";
import { useState, useCallback } from 'react';
import Chat from '@/components/Chat';
import ChatSidebar from '@/components/ChatSidebar';
import { ThemeToggle } from '@/components/theme-toggle';
import { Button } from '@/components/ui/button';
import { PlusCircle } from 'lucide-react';
export default function Home() {
const [activeChatId, setActiveChatId] = useState<string | undefined>(undefined);
const [refreshCounter, setRefreshCounter] = useState(0);
const handleChatSelect = (chatId: string) => {
if (chatId === "") {
setActiveChatId(undefined);
} else {
setActiveChatId(chatId);
}
};
const handleNewChat = () => {
setActiveChatId(undefined);
};
// Function to trigger a sidebar refresh
const refreshSidebar = useCallback(() => {
setRefreshCounter(prev => prev + 1);
}, []);
return (
<div className="flex justify-center bg-background min-h-screen">
<main className="min-h-screen flex max-w-6xl w-full shadow-sm">
<ChatSidebar
activeChatId={activeChatId}
onChatSelect={handleChatSelect}
refreshTrigger={refreshCounter}
onChatDeleted={refreshSidebar}
/>
<div className="flex-1 p-8 flex flex-col">
<div className="w-full max-w-2xl mx-auto">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">AI Chat with MongoDB Memory</h1>
<p className="text-muted-foreground">
Your conversations are saved with MongoDB for persistent chat history
</p>
</div>
<div className="flex items-center gap-4">
<Button
variant="outline"
size="sm"
className="gap-2"
onClick={handleNewChat}
>
<PlusCircle className="h-4 w-4" />
New chat
</Button>
<ThemeToggle />
</div>
</div>
<Chat
chatId={activeChatId}
onChatCreated={refreshSidebar}
onMessageSent={refreshSidebar}
/>
</div>
</div>
</main>
</div>
);
}
The home page component serves as the application's layout coordinator:
- State management:
- Maintains the active chat ID
- Handles chat selection and creation
- Manages sidebar refresh state
- Component integration:
- Connects ChatSidebar and Chat components
- Handles communication between components
- Manages theme toggling
- Layout structure:
- Sidebar on the left
- Main chat area on the right
- Header with title and actions
The refresh pattern used here is particularly effective—when the chat component needs to refresh the sidebar (after creating a new chat or adding a message), it calls the refreshSidebar
function, which increments the counter. This change triggers a reload of the chat list in the sidebar.
Note: The refresh pattern used in this example is intentionally simple for clarity. In a production application, you might want to use more sophisticated data fetching libraries like SWR, React Query, or Next.js's built-in data fetching mechanisms for more efficient cache invalidation, optimistic updates, and automatic revalidation.
Testing the application
Now that we have all our components in place, let's test the application:
Start your Next.js development server:
npm run dev
Navigate to http://localhost:3000
in your browser, and you should see your chat interface with the history sidebar. Here's what you can try:
- Start a new conversation by typing in the input field.
- Refresh the page—notice how your chat history persists.
- Create another new chat and add some messages.
- Use the sidebar to switch between different conversations.
- Delete a chat you no longer need.
- Toggle between light and dark themes.
When testing your application, pay attention to how the data persists between page reloads and browser sessions. This persistent memory is the core feature we've built, and it's what makes this chat application truly useful compared to ones that forget everything when you refresh.
Next steps
Now that you have a working chat application with persistent memory, you could extend it with features like:
- User authentication.
- AI-generated chat titles.
- Media file support.
- Export functionality.
- Conversation branching.
- Custom AI instructions.
- Retrival-augmented generation (RAG) support.
Each of these extensions can build upon the foundation we've created, leveraging MongoDB's flexibility to store additional data as needed.
Final thoughts
Building chat apps with memory might seem daunting at first, but MongoDB makes it surprisingly straightforward. The document model is a natural fit for storing conversation threads, and the MongoDB Atlas free tier gives you plenty of room to experiment without worrying about costs.
The combination of Next.js, Vercel AI SDK, and MongoDB creates a powerful foundation for building sophisticated AI chat applications that remember their conversations. The server actions approach in Next.js App Router provides a clean, type-safe way to interact with the database, while the Vercel AI SDK handles the complexities of streaming AI responses.
What makes this architecture particularly valuable is its scalability. As your application grows, MongoDB Atlas can scale with you, from the free tier all the way to enterprise-grade deployments across multiple regions. The document model adapts easily to evolving requirements, allowing you to add new features without major restructuring.
I hope this tutorial helps you build something amazing!
Say Hello! YouTube | Twitter | LinkedIn | Instagram | TikTok
Top comments (0)