The WhatsApp Chat application is built with Next.js 15, React 19, and Zustand for state management. This page explains the architectural patterns and how components work together to create a seamless chat experience.
Next.js app structure
The application uses the Next.js App Router with a client-side rendered architecture:
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} font-sans antialiased bg-app-surface`}>
{children}
</body>
</html>
);
}
// app/page.tsx
import { ChatApp } from "@/components/chat/chat-app";
export default function Home() {
return (
<main className="flex min-h-screen items-stretch bg-app-surface">
<ChatApp />
</main>
);
}
The entire chat interface is rendered within a single <ChatApp /> component, which serves as the main orchestrator.
Component hierarchy
The component tree follows a clear hierarchy that separates concerns:
ChatApp (Root)
The root component that manages global state subscriptions and coordinates between sidebar and chat panel.// components/chat/chat-app.tsx
export function ChatApp() {
const isMobile = useIsMobile()
const chats = useChatStore((state) => state.chats)
const contacts = useChatStore((state) => state.contacts)
const activeChatId = useChatStore((state) => state.activeChatId)
useChatSimulator() // Simulates incoming messages
return (
<div className="flex h-screen w-full overflow-hidden">
<ChatSidebar />
<ChatPanel />
</div>
)
}
ChatSidebar
Displays the list of conversations, search functionality, and user profile. Handles chat selection and search queries.
ChatPanel
The main conversation view that contains:
ChatHeader - Contact info and actions
MessageList - Virtualized message timeline
MessageComposer - Input field for sending messages
// components/chat/chat-panel.tsx
export function ChatPanel({ chat, contact, messages, ... }: ChatPanelProps) {
return (
<section className="flex h-full flex-1 flex-col">
<ChatHeader contact={contact} isOnline={contact.isOnline} />
<MessageList messages={messages} profileId={profileId} />
<MessageComposer chatId={chat.id} onSend={onSendMessage} />
</section>
)
}
MessageList
Uses React Virtuoso for performant rendering of long message histories. Groups messages by date and handles avatar display logic.
MessageBubble
Individual message rendering with support for text, images, and audio. Shows message status (queued, sending, sent, delivered, read).
Zustand state management
The application uses a single Zustand store (useChatStore) for all state management:
// lib/chat/state.ts
export interface ChatStore extends ChatStateSnapshot {
profile: Profile
searchQuery: string
showArchived: boolean
// Actions
setActiveChat: (chatId?: string) => void
sendComposerPayload: (chatId: string, payload: ComposerPayload) => string | undefined
receiveMessage: (chatId: string, message: Message) => void
markChatAsRead: (chatId: string) => void
setTypingIndicator: (chatId: string, authorId: string, isTyping: boolean) => void
}
export const useChatStore = create<ChatStore>()(
persist(
(set, get) => ({
...initialState,
setActiveChat: (chatId) => { /* ... */ },
sendComposerPayload: (chatId, payload) => { /* ... */ },
// ... other actions
}),
{
name: "chat-store",
version: 1,
storage: createJSONStorage(() => localStorage),
}
)
)
The store uses Zustand’s persist middleware to save state to localStorage, ensuring conversations persist across page refreshes.
State structure
The core state shape is defined in ChatStateSnapshot:
export interface ChatStateSnapshot {
chats: Record<string, Chat> // Chat metadata by ID
messages: Record<string, Message> // All messages by ID
contacts: Record<string, Contact> // Contact profiles by ID
drafts: Record<string, DraftMessage> // Unsent message drafts
typingIndicators: TypingIndicator[] // Who's typing in which chat
searchHistory: ChatSearchHistoryItem[] // Recent searches
activeChatId?: string // Currently selected chat
}
Selectors
The store exports pre-built selectors for common queries:
export const chatSelectors = {
chatList: (state: ChatStore) => Object.values(state.chats),
activeChat: (state: ChatStore) =>
state.activeChatId ? state.chats[state.activeChatId] : undefined,
activeMessages: (state: ChatStore) => {
if (!state.activeChatId) return []
const chat = state.chats[state.activeChatId]
return chat.messageIds.map((id) => state.messages[id]).filter(Boolean)
},
}
Data flow patterns
Sending messages
Receiving messages
State subscriptions
When a user sends a message:
MessageComposer calls onSend prop with message payload
ChatApp calls sendComposerPayload action
- Store creates message with
queued status
- Store schedules automatic status progression
- Message appears instantly in
MessageList
const handleSendMessage = (chatId: string, payload: { text?: string }) => {
sendComposerPayload(chatId, payload)
}
Incoming messages are handled by receiveMessage action:receiveMessage: (chatId, message) => {
set((state) => {
const chat = state.chats[chatId]
return {
messages: { ...state.messages, [message.id]: message },
chats: {
...state.chats,
[chatId]: {
...chat,
messageIds: [...chat.messageIds, message.id],
unreadCount: state.activeChatId === chatId ? 0 : chat.unreadCount + 1,
},
},
}
})
}
Components subscribe to specific slices of state to minimize re-renders:// Only re-renders when active chat changes
const activeChatId = useChatStore((state) => state.activeChatId)
// Only re-renders when messages object changes
const messages = useChatStore((state) => state.messages)
// Memoized derived state
const activeMessages = useMemo(() => {
if (!activeChatId) return []
const chat = chats[activeChatId]
return chat.messageIds.map((id) => messages[id]).filter(Boolean)
}, [activeChatId, chats, messages])
The application implements several performance optimizations:
- Virtualized lists using React Virtuoso for message rendering
- Normalized state with separate dictionaries for chats, messages, and contacts
- Selective subscriptions to prevent unnecessary re-renders
- Memoized computations for derived state like sorted chat lists
Message virtualization
// components/chat/message-list.tsx
import { Virtuoso } from "react-virtuoso"
export function MessageList({ messages, ... }) {
const timeline = useMemo(() => buildTimeline(messages, profileId), [messages, profileId])
return (
<Virtuoso
data={timeline}
followOutput="smooth"
alignToBottom
itemContent={(index, item) => (
<MessageBubble message={item.message} />
)}
/>
)
}
Mobile responsiveness
The application adapts to mobile screens using conditional rendering:
const isMobile = useIsMobile()
return (
<div className="flex h-screen">
<div className={cn(
"hidden md:flex",
(!isMobile || !activeChatId) && "flex"
)}>
<ChatSidebar />
</div>
<div className={cn(
"flex-1",
isMobile && !activeChatId && "hidden"
)}>
<ChatPanel onBack={isMobile ? () => setActiveChat(undefined) : undefined} />
</div>
</div>
)
On mobile, only one panel is visible at a time with a back button to return to the sidebar.