Advanced

Connection State

Monitor WebSocket connection state for offline and reconnection UI.

Basic Usage

<script setup lang="ts">
const { isConnected, isReconnecting, connectionRetries } = useConvexConnectionState()
</script>

<template>
  <div v-if="isReconnecting" class="offline-banner">
    Reconnecting... (attempt {{ connectionRetries }})
  </div>
</template>

API Reference

Enable module-level logging via the logging config option to track connection state changes.

Returns

PropertyTypeDescription
stateReadonly<Ref<ConnectionState>>Full connection state object
isConnectedComputedRef<boolean>WebSocket currently connected
hasEverConnectedComputedRef<boolean>Has successfully connected before
connectionRetriesComputedRef<number>Number of retry attempts
hasInflightRequestsComputedRef<boolean>Has pending requests
isReconnectingComputedRef<boolean>Was connected, now disconnected
inflightMutationsComputedRef<number>Pending mutations count
inflightActionsComputedRef<number>Pending actions count

ConnectionState Type

interface ConnectionState {
  hasInflightRequests: boolean
  isWebSocketConnected: boolean
  timeOfOldestInflightRequest: Date | null
  hasEverConnected: boolean
  connectionCount: number
  connectionRetries: number
  inflightMutations: number
  inflightActions: number
}

Patterns

Offline Banner

<script setup lang="ts">
const { isConnected, hasEverConnected } = useConvexConnectionState()

// Only show banner if we were connected before
const showOfflineBanner = computed(() =>
  hasEverConnected.value && !isConnected.value
)
</script>

<template>
  <Transition name="slide">
    <div v-if="showOfflineBanner" class="offline-banner">
      <span>You're offline. Changes will sync when reconnected.</span>
    </div>
  </Transition>
</template>

<style scoped>
.offline-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  background: #f59e0b;
  color: white;
  padding: 0.5rem;
  text-align: center;
  z-index: 1000;
}

.slide-enter-active,
.slide-leave-active {
  transition: transform 0.3s ease;
}

.slide-enter-from,
.slide-leave-to {
  transform: translateY(-100%);
}
</style>

Reconnecting Indicator

<script setup lang="ts">
const { isReconnecting, connectionRetries } = useConvexConnectionState()
</script>

<template>
  <div v-if="isReconnecting" class="reconnecting">
    <div class="spinner" />
    <span>
      Reconnecting
      <span v-if="connectionRetries > 0">
        (attempt {{ connectionRetries + 1 }})
      </span>
    </span>
  </div>
</template>

<style scoped>
.reconnecting {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  background: #fef3c7;
  border-radius: 0.375rem;
}

.spinner {
  width: 1rem;
  height: 1rem;
  border: 2px solid #d97706;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}
</style>

Pending Operations Badge

<script setup lang="ts">
const { inflightMutations, inflightActions, hasInflightRequests } = useConvexConnectionState()

const pendingCount = computed(() =>
  inflightMutations.value + inflightActions.value
)
</script>

<template>
  <div v-if="hasInflightRequests" class="sync-indicator">
    <div class="sync-spinner" />
    <span>Syncing {{ pendingCount }} changes...</span>
  </div>
</template>

Connection Status in Header

<script setup lang="ts">
const { isConnected, isReconnecting, hasEverConnected } = useConvexConnectionState()

const statusColor = computed(() => {
  if (!hasEverConnected.value) return 'gray'
  if (isConnected.value) return 'green'
  if (isReconnecting.value) return 'yellow'
  return 'red'
})

const statusText = computed(() => {
  if (!hasEverConnected.value) return 'Connecting...'
  if (isConnected.value) return 'Connected'
  if (isReconnecting.value) return 'Reconnecting...'
  return 'Offline'
})
</script>

<template>
  <header class="app-header">
    <div class="logo">MyApp</div>

    <div class="connection-status" :class="statusColor">
      <span class="status-dot" />
      {{ statusText }}
    </div>
  </header>
</template>

<style scoped>
.connection-status {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.875rem;
}

.status-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
}

.green .status-dot { background: #10b981; }
.yellow .status-dot { background: #f59e0b; animation: pulse 1s infinite; }
.red .status-dot { background: #ef4444; }
.gray .status-dot { background: #9ca3af; }

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}
</style>

Save Status Indicator

<script setup lang="ts">
const { inflightMutations, isConnected } = useConvexConnectionState()

const saveStatus = computed(() => {
  if (!isConnected.value) return 'offline'
  if (inflightMutations.value > 0) return 'saving'
  return 'saved'
})
</script>

<template>
  <div class="save-status">
    <template v-if="saveStatus === 'saving'">
      <div class="spinner-small" />
      Saving...
    </template>
    <template v-else-if="saveStatus === 'saved'">
      <span class="check">&#10003;</span>
      Saved
    </template>
    <template v-else>
      <span class="warning">&#9888;</span>
      Offline
    </template>
  </div>
</template>

Full State Object

For advanced use cases, access the complete state object:

<script setup lang="ts">
const { state } = useConvexConnectionState()

// Debug: log all state changes
watch(state, (newState) => {
  console.log('Connection state:', {
    connected: newState.isWebSocketConnected,
    retries: newState.connectionRetries,
    pendingMutations: newState.inflightMutations,
    pendingActions: newState.inflightActions,
    oldestRequest: newState.timeOfOldestInflightRequest,
  })
})
</script>
Enable logging.enabled in your Nuxt config to automatically log all connection:change events without manual watchers.

Notes

  • Connection state is only available on the client
  • During SSR, all values show disconnected state
  • The composable subscribes to Convex's internal connection events
  • Cleanup is handled automatically on component unmount