Guides

Realtime

Update data instantly when changes happen on the server.

Realtime Provider

Use a Realtime provider to enable instant updates in your application. This provider handles subscriptions to events (like record creation, updates, or deletions) and publishing custom events.

The Realtime interface defines the contract:

interface Realtime {
    // Subscribe to updates
    subscribe: (props: SubscribeProps) => UnsubscribeFn
    // Unsubscribe from updates (optional)
    unsubscribe?: (props: UnsubscribeProps) => void
    // Publish a custom event (optional)
    publish?: (event: RealtimeEvent) => void
}

Modes

There are two main ways to handle realtime events: Auto and Manual.

ModeHow It WorksWhen to Use
AutoFramework automatically invalidates queries and refetches dataMost common - reduces boilerplate code
ManualYou handle events in callbacks with full controlComplex logic - conditional updates, multiple actions

Auto Mode

In auto mode, the framework automatically invalidates related queries when an event is received. For example, if a "post" is updated on the server, any active useGetList or useGetOne queries for that post will be refetched automatically.

Scenario: Real-time collaborative editor where multiple users edit the same document.

<script setup lang="ts">
import { useGetOne } from '@ginjou/vue'

// Auto mode (default)
const { data: post } = useGetOne({
    resource: 'posts',
    id: '123',
})

// When another user updates this post on server:
// 1. Event is received
// 2. Query cache is invalidated
// 3. useGetOne automatically refetches
// 4. UI updates with latest data
</script>

<template>
    <div>
        <h1>{{ post?.title }}</h1>
        <p>Last updated: {{ post?.updatedAt }}</p>
    </div>
</template>

Manual Mode

In manual mode, you handle the event yourself in a callback. This provides total control over how the UI responds to changes.

Scenario: Live chat application where you need to insert new messages at the top of the list, not refetch all messages.

<script setup lang="ts">
import { useSubscribe } from '@ginjou/vue'
import { ref } from 'vue'

const messages = ref([])

useSubscribe({
    channel: 'chat/room-123',
    actions: ['created'],
    mode: 'manual',
    callback: (event) => {
        // Control exactly what happens
        if (event.action === 'created') {
            // Insert new message at the top
            messages.value.unshift(event.payload)
        }
    },
})
</script>

<template>
    <div class="messages">
        <div v-for="msg in messages" :key="msg.id">
            {{ msg.content }}
        </div>
    </div>
</template>

Data Flow

Realtime events automatically integrate with data queries in auto mode:

  • useGetOne: Refetches when a single record is updated
  • useGetList: Refetches when records are created, updated, or deleted
  • useGetMany: Refetches when any of the specified records change
  • useGetInfiniteList: Refetches affected pages when records change

In manual mode, you explicitly handle the event and decide whether to refetch, update local state, or perform other actions.

Subscribe

Use useSubscribe to listen for events on a specific channel or resource.

<script setup lang="ts">
import { useSubscribe } from '@ginjou/vue'

useSubscribe({
    channel: 'resources/posts',
    actions: ['created', 'updated', 'deleted'],
    callback: (event) => {
        console.log('Realtime event received:', event)
        // event.action: 'created' | 'updated' | 'deleted'
        // event.payload: The changed record data
    },
})
</script>

Example: Subscribe to Specific Resource Changes

<script setup lang="ts">
import { useSubscribe } from '@ginjou/vue'
import { ref } from 'vue'

const status = ref('idle')

useSubscribe({
    channel: 'resources/orders',
    actions: ['updated'],
    mode: 'manual',
    callback: (event) => {
        if (event.payload.status === 'completed') {
            status.value = 'Order completed!'
        }
    },
})
</script>

<template>
    <p>{{ status }}</p>
</template>

Publish

Use usePublish to send custom events to other users or components. Publishing is automatically called by mutation operations in auto mode, but you can also publish custom events manually.

OperationAuto-PublishesEvent Type
useCreateOne / useCreateManyYes'created'
useUpdateOne / useUpdateManyYes'updated'
useDeleteOne / useDeleteManyYes'deleted'

Manual Publishing:

<script setup lang="ts">
import { usePublish } from '@ginjou/vue'

const publish = usePublish()

function handleInteraction() {
    // Publish custom event
    publish({
        channel: 'collaboration/doc-123',
        type: 'cursor_move',
        payload: { userId: '456', x: 100, y: 200 },
    })
}
</script>

<template>
    <button @click="handleInteraction">
        Move cursor
    </button>
</template>

Example: Collaborative Presence

<script setup lang="ts">
import { usePublish, useSubscribe } from '@ginjou/vue'
import { onUnmounted, ref } from 'vue'

const userId = '123'
const activeCursors = ref([])
const publish = usePublish()

// Publish cursor position every 100ms
let publishInterval = setInterval(() => {
    publish({
        channel: 'collaboration/doc-456',
        type: 'cursor_position',
        payload: { userId, x: 100, y: 200 },
    })
}, 100)

// Subscribe to other users' cursors
useSubscribe({
    channel: 'collaboration/doc-456',
    actions: [],
    mode: 'manual',
    callback: (event) => {
        if (event.type === 'cursor_position' && event.payload.userId !== userId) {
            activeCursors.value = [
                ...activeCursors.value.filter(c => c.userId !== event.payload.userId),
                event.payload,
            ]
        }
    },
})

onUnmounted(() => {
    publishInterval
    && clearInterval(publishInterval)
    publishInterval = undefined
})
</script>

<template>
    <div class="cursors">
        <div
            v-for="cursor in activeCursors" :key="cursor.userId"
            :style="{ left: `${cursor.x}px`, top: `${cursor.y}px` }"
        >
            👆
        </div>
    </div>
</template>