Projects
Jan 12, 2026

Temporary Messages System

Dual-state messages for optimized user experience and data consistency

This article explains the temporary messages that optimistically shows messages immediately to users while ensuring they are properly persisted to the database.

Optimistic updates are a pattern where the frontend "predicts" success to provide an instantaneous user experience.

The Problem

Without temporary messages, users experience significant lag when sending messages:

Traditional flow:

User types → User sends →
Wait for server... →
Database saves →
Server responds →
User sees message

Feels slow

With temporary messages:

Optimized flow:

User types → User sends →
INSTANTLY show message (temp) →
Background: Save to database →
LATER: Replace temp with persisted version

Feels instant

Architecture

Temporary Message Lifecycle

Implementation

Creating Temporary Message

When the user clicks "Send", the frontend generates a Temporary ID and pushes the message to the list with a SENDING status. This happens on the client-side memory, and there is no server involvement for temporary messages.

The client stores these "in-flight" messages in memory. This allows the application to keep track of which UI elements are waiting for a database confirmation without relying on the server to keep that state.

The actual message is sent to the server via the MESSAGE_SEND event.

// Frontend: Composition
const messages = ref<Message[]>([])
const tempMessages = new Map<string, Message>()

const sendMessage = (content: string) => {
  const tempId = `temp-${Date.now()}-${Math.random()}`

  // Create optimistic message
  const tempMessage: Message = {
    id: tempId, // Temporary ID
    content,
    sender_id: currentUser.id,
    created_at: new Date(),
    status: 'SENDING', // Not yet persisted
    temporary: true,
  }

  // Show immediately (optimistic update)
  messages.value.push(tempMessage)
  tempMessages.set(tempId, tempMessage)

  // Scroll to message
  scrollToBottom()

  // Send to server
  socket.emit(
    'MESSAGE_SEND',
    {
      tempId,
      content,
      channelId: currentChannel.id,
    },
    response => {
      if (!response.success) {
        // Remove if send failed
        removeMessage(tempId)
        showError('Failed to send message')
      }
    }
  )
}

Updating Persisted Message

Once the server responds, the client reacts based on a success or failure.

In case of success, the client uses the tempId to find the local message and swaps it with the real database record, with the actual message ID and delivered status.

In case of failure, the client shows a "failed to send" status.

// Frontend: Handle persisted message
socket.on('messagePersisted', data => {
  const { tempId, messageId, message } = data

  // Find the temporary message
  const index = messages.value.findIndex(m => m.id === tempId)

  if (index !== -1) {
    // Replace temp message with persisted version
    messages.value[index] = {
      ...message,
      id: messageId,
      status: 'delivered',
      temporary: false,
    }
  }

  // Clean up temp storage
  tempMessages.delete(tempId)
})

// Handle message failure
socket.on('messageFailed', data => {
  const { tempId } = data

  const message = messages.value.find(m => m.id === tempId)
  if (message) {
    message.status = 'failed'
  }
})

Message States and Transitions

Message State Transitions