Realtime in React
@vaifstudio/react wraps the realtime client in hooks. Every hook uses the client’s one shared connection, subscribes when the component mounts, and cleans up when it unmounts or its inputs change. React StrictMode’s double mount does not open a second socket.
Provider
Section titled “Provider”Create the client once and pass it to VaifProvider:
import { createVaifClient } from "@vaifstudio/client";import { VaifProvider } from "@vaifstudio/react";import type { ReactNode } from "react";
const vaif = createVaifClient({ projectId: import.meta.env.VITE_VAIF_PROJECT_ID, apiKey: import.meta.env.VITE_VAIF_PUBLISHABLE_KEY,});
export function App({ children }: { children: ReactNode }) { return <VaifProvider client={vaif}>{children}</VaifProvider>;}Row changes: useRealtime
Section titled “Row changes: useRealtime”import { useState } from "react";import { useRealtime } from "@vaifstudio/react";
type Todo = { id: string; list_id: string; title: string; done: boolean };
export function TodoList({ listId }: { listId: string }) { const [todos, setTodos] = useState<Todo[]>([]);
const { status, error } = useRealtime<Todo>("todos", { event: ["INSERT", "DELETE"], filter: { field: "list_id", value: listId }, onInsert: (todo) => setTodos((all) => [...all, todo]), onDelete: (todo) => setTodos((all) => all.filter((t) => t.id !== todo.id)), });
if (error) return <p>Live updates are unavailable: {error.message}</p>; return ( <ul data-live={status === "SUBSCRIBED"}> {todos.map((t) => ( <li key={t.id}>{t.title}</li> ))} </ul> );}| Option | Meaning |
|---|---|
event |
"*" (default), one operation, or an array such as ["INSERT", "UPDATE"] |
filter |
{ field, operator?, value }, an array of them (ANDed), or a string such as "list_id=42" or "total=gte.100" |
columns |
Only receive these columns |
onInsert, onUpdate, onDelete, onChange |
Callbacks. They can be inline functions: changing one does not resubscribe |
enabled |
false pauses the subscription (status becomes "IDLE") |
It returns { status, isSubscribed, error, lastEvent }. useSubscription(table, options) is the same subscription with the changes collected for you: { data, changes, isSubscribed, status, error, clearChanges }.
Broadcast: useChannel and useBroadcast
Section titled “Broadcast: useChannel and useBroadcast”import { useBroadcast } from "@vaifstudio/react";
type Reaction = { emoji: string };
export function Reactions({ roomId }: { roomId: string }) { const { send, messages } = useBroadcast<Reaction>(`room:${roomId}`);
return ( <div> <button onClick={() => send("reaction", { emoji: "👍" })}>👍</button> {messages.map((m, i) => ( <span key={i}>{m.payload.emoji}</span> ))} </div> );}useChannel(name, { type, maxMessages, enabled }) returns the lower-level { broadcast, messages, lastMessage, isJoined, error, leave }.
Presence: usePresence
Section titled “Presence: usePresence”import { usePresence } from "@vaifstudio/react";
type Viewer = { name: string };
export function WhoIsHere({ name }: { name: string }) { const { count, presence } = usePresence<Viewer>("document:123", { name });
return ( <p> {count} viewing: {Object.values(presence).flat().map((p) => p.state.name).join(", ")} </p> );}Presence is per connection across the whole project; see Channels & Presence for what that means for rooms.
Connection status: useRealtimeConnection
Section titled “Connection status: useRealtimeConnection”import { useRealtimeConnection } from "@vaifstudio/react";
export function ConnectionBadge() { const { state, error, reconnect } = useRealtimeConnection(); if (error) return <button onClick={reconnect}>Offline: {error.message}. Retry</button>; return <span>{state}</span>;}React Native and Expo
Section titled “React Native and Expo”@vaifstudio/expo has the same hooks, and closes the shared connection while the app is in the background.