Composables

useRealtimeState

Share reactive state across all connected clients.

useRealtimeState

Share reactive state across all connected clients. Works just like Vue's useState but automatically synchronizes changes in real-time.

Usage

<script setup>
const counter = useRealtimeState('counter', 0)
</script>

<template>
  <button @click="counter++">
    Count: {{ counter }}
  </button>
</template>

Type Signature

function useRealtimeState<T>(
  key: string,
  defaultValue?: T,
  options?: UseRealtimeStateOptions
): UseRealtimeStateReturn<T>

Parameters

key

  • Type: string
  • Required: Yes

A unique identifier for the state. All clients using the same key will share the same state.

// Different keys = different state
const counter = useRealtimeState('counter', 0)
const users = useRealtimeState('online-users', [])
const settings = useRealtimeState('app-settings', { theme: 'dark' })

defaultValue

  • Type: T
  • Required: No

The initial value to use if no state exists on the server. If the server already has a value for this key, that value will be used instead.

// Primitives
const count = useRealtimeState('count', 0)
const name = useRealtimeState('name', 'Anonymous')
const enabled = useRealtimeState('enabled', true)

// Objects
const profile = useRealtimeState('profile', {
  name: 'User',
  avatar: '/default.png',
})

// Arrays
const messages = useRealtimeState('messages', [])

options

  • Type: UseRealtimeStateOptions
  • Required: No
interface UseRealtimeStateOptions {
  /**
   * Whether to optimistically update the value on update
   * @default true
   */
  optimisticUpdates?: boolean

  /**
   * The timeout for value updates in milliseconds
   * @default 5000
   */
  updateTimeout?: number
}

Return Value

Returns a WritableComputedRef<T> with additional properties:

interface UseRealtimeStateReturn<T> extends WritableComputedRef<T> {
  /**
   * Loading state - true while fetching initial value
   */
  loading: Readonly<Ref<boolean>>

  /**
   * Manually refresh the value from the server
   */
  refresh: () => void
}

value

Access and update the state value:

const counter = useRealtimeState('counter', 0)

// Read
console.log(counter.value) // 0

// Write (syncs to all clients)
counter.value = 10
counter.value++

loading

A readonly ref indicating whether the initial value is being fetched:

<script setup>
const data = useRealtimeState('data', null)
</script>

<template>
  <div v-if="data.loading.value">Loading...</div>
  <div v-else>{{ data }}</div>
</template>

refresh()

Manually fetch the latest value from the server:

const counter = useRealtimeState('counter', 0)

// Force refresh from server
counter.refresh()

Examples

Counter

A simple counter that syncs across all clients:

Counter.vue
<script setup>
const counter = useRealtimeState('counter', 0)
</script>

<template>
  <div class="flex items-center gap-4">
    <button @click="counter--">-</button>
    <span>{{ counter }}</span>
    <button @click="counter++">+</button>
  </div>
</template>

Shared Form

A form where all fields sync in real-time:

SharedForm.vue
<script setup>
const form = useRealtimeState('contact-form', {
  name: '',
  email: '',
  message: '',
})
</script>

<template>
  <form>
    <input v-model="form.name" placeholder="Name" />
    <input v-model="form.email" type="email" placeholder="Email" />
    <textarea v-model="form.message" placeholder="Message" />
  </form>
</template>

Online Users List

Track online users across sessions:

OnlineUsers.vue
<script setup>
const onlineUsers = useRealtimeState('online-users', [])
const currentUser = { id: crypto.randomUUID(), name: 'User' }

onMounted(() => {
  onlineUsers.value = [...onlineUsers.value, currentUser]
})

onUnmounted(() => {
  onlineUsers.value = onlineUsers.value.filter(u => u.id !== currentUser.id)
})
</script>

<template>
  <ul>
    <li v-for="user in onlineUsers" :key="user.id">
      {{ user.name }}
    </li>
  </ul>
</template>

With Loading State

Show a loading indicator while fetching initial state:

WithLoading.vue
<script setup>
const data = useRealtimeState('large-dataset', [], {
  updateTimeout: 10000, // Longer timeout for large data
})
</script>

<template>
  <div>
    <div v-if="data.loading.value" class="animate-pulse">
      Loading data...
    </div>
    <div v-else>
      <p>{{ data.length }} items loaded</p>
      <!-- render data -->
    </div>
  </div>
</template>

Disable Optimistic Updates

For critical data where you want to wait for server confirmation:

CriticalData.vue
<script setup>
const balance = useRealtimeState('account-balance', 0, {
  optimisticUpdates: false, // Wait for server confirmation
})

async function withdraw(amount) {
  // Value won't update until server confirms
  balance.value = balance.value - amount
}
</script>

How It Works

  1. Initial Load: When the composable is created, it fetches the current value from the server
  2. Subscription: The client subscribes to updates for the specified key
  3. Updates: When you modify the value:
    • If optimisticUpdates is true (default), the UI updates immediately
    • The change is sent to the server
    • The server broadcasts the update to all other subscribed clients
    • If the server rejects the update, the value is rolled back
  4. Cleanup: When the component unmounts, the subscription is automatically removed