💬Intermediate

Realtime Chat

A live group chat app powered by WebSocket subscriptions. Messages sent by any user appear instantly for everyone connected — no polling, no page refresh.

ViteReactTypeScriptWebSocketsRealtime

How it works

1. Load

Fetch the last 50 messages with getList() on mount.

2. Subscribe

Open a WebSocket connection with subscribe(). Events arrive for every create and delete.

3. Update state

Add or remove messages from local state — React re-renders automatically.

subscribe() returns an unsubscribe function

Always clean up subscriptions in the useEffect return to avoid memory leaks. The unsubscribe function is async — call it with unsub?.().

Setup

cp .env.example .env
# VITE_TACOBASE_URL=https://your-app.tacobase.dev
# VITE_TACOBASE_API_KEY=tbk_...

npm install
npm run dev

The realtime pattern

The core of the example. Load initial data, then subscribe and update local state on every event.

import { useClient } from '@tacobase/react'

function Chat({ userId }: { userId: string }) {
  const client = useClient()
  const [messages, setMessages] = useState<Message[]>([])

  useEffect(() => {
    let unsub: (() => Promise<void>) | null = null

    async function init() {
      // 1. Load recent messages
      const initial = await client.collection('messages').getList(1, 50, {
        sort: 'created',
      })
      setMessages(initial.items as Message[])

      // 2. Subscribe to realtime updates
      unsub = await client.collection('messages').subscribe((event) => {
        if (event.action === 'create') {
          setMessages(prev => [...prev, event.record as Message])
        }
        if (event.action === 'delete') {
          setMessages(prev => prev.filter(m => m.id !== event.record.id))
        }
      })
    }

    init()

    // Cleanup: unsubscribe on unmount
    return () => { unsub?.() }
  }, [client])

  async function sendMessage(text: string) {
    await client.collection('messages').create({ text, author: userId })
  }

  // ...
}