Skip to main content
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:
1

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>
  )
}
2

ChatSidebar

Displays the list of conversations, search functionality, and user profile. Handles chat selection and search queries.
3

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>
  )
}
4

MessageList

Uses React Virtuoso for performant rendering of long message histories. Groups messages by date and handles avatar display logic.
5

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

When a user sends a message:
  1. MessageComposer calls onSend prop with message payload
  2. ChatApp calls sendComposerPayload action
  3. Store creates message with queued status
  4. Store schedules automatic status progression
  5. Message appears instantly in MessageList
const handleSendMessage = (chatId: string, payload: { text?: string }) => {
  sendComposerPayload(chatId, payload)
}

Performance optimizations

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.

Build docs developers (and LLMs) love