Advanced

Connection State

Monitor WebSocket connection state for offline and reconnection UI.

Basic Usage

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

<template>
  <div v-if="isReconnecting && shouldShowOfflineUi" class="offline-banner">Reconnecting...</div>
</template>
useConvexConnectionState() includes shouldShowOfflineUi, which already handles the initial client hydration grace window to prevent offline-banner flash.

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
isReconnectingComputedRef<boolean>Was connected, now disconnected
shouldShowOfflineUiComputedRef<boolean>Offline UI flag with hydration flash suppression
pendingMutationsComputedRef<number>Pending mutations count
pendingActionsComputedRef<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 { shouldShowOfflineUi, state } = useConvexConnectionState()

// Only show banner if we were connected before
const showOfflineBanner = computed(() => state.value.hasEverConnected && shouldShowOfflineUi.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, state } = useConvexConnectionState()
</script>

<template>
  <div v-if="isReconnecting" class="reconnecting">
    <div class="spinner" />
    <span>
      Reconnecting
      <span v-if="state.connectionRetries > 0"> (attempt {{ state.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 { pendingMutations, pendingActions, state } = useConvexConnectionState()

const pendingCount = computed(() => pendingMutations.value + pendingActions.value)
</script>

<template>
  <div v-if="state.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, state } = useConvexConnectionState()

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

const statusText = computed(() => {
  if (!state.value.hasEverConnected) 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 { pendingMutations, isConnected } = useConvexConnectionState()

const saveStatus = computed(() => {
  if (!isConnected.value) return 'offline'
  if (pendingMutations.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: 'info' (or 'debug') 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