All hooks are imported from @spaces/sdk/storage. They must be used inside a RecordProvider (which main.tsx sets up for you).
Data Hooks
useQuery<T>(collection, options?)
Subscribe to records in a collection with real-time updates. This is the primary way to read data.
const { records, status, error } = useQuery<Task>('tasks', {
where: { status: 'active' },
orderBy: 'createdAt',
orderDir: 'desc',
limit: 50,
})
| Option | Type | Description |
|---|
where | Record<string, any> | Filter by field values |
orderBy | string | Field to sort by |
orderDir | 'asc' | 'desc' | Sort direction |
limit | number | Maximum records to return |
Returns:
| Field | Type | Description |
|---|
records | RecordData<T>[] | Matching records. Each has .id, .data, .createdAt, .updatedAt |
status | 'loading' | 'ready' | 'error' | Subscription state |
error | string | undefined | Error message if status is 'error' |
The query stays subscribed — when records change on the server, your component re-renders automatically.
useMutations<T>(collection)
Get functions to create, update, and delete records.
const { create, put, remove } = useMutations<Task>('tasks')
// Create a record
const id = await create({ title: 'Buy groceries', completed: false })
// Update a record
await put(id, { title: 'Buy groceries', completed: true })
// Delete a record
await remove(id)
Returns:
| Function | Description |
|---|
create(data) | Creates a record, returns the new id. Fire-and-forget. |
put(id, data) | Replaces a record’s data. Fire-and-forget. |
remove(id) | Deletes a record. Fire-and-forget. |
createConfirmed(data) | Like create, but waits for server acknowledgment. |
putConfirmed(id, data) | Like put, but waits for server acknowledgment. |
removeConfirmed(id) | Like remove, but waits for server acknowledgment. |
Use the fire-and-forget versions for UI-driven actions (toggling a checkbox, adding items). Use the Confirmed versions when you need to know the write succeeded before proceeding (e.g., creating a record and then navigating to its detail page).
User Hooks
useUser()
Get the current authenticated user with their profile and role in this room.
const { user, isLoading, refetch } = useUser()
user includes:
| Field | Description |
|---|
id | User ID |
name | Display name |
email | Email address |
imageUrl | Avatar URL |
role | Role in this room ('viewer', 'member', 'admin') |
credits | { total, subscription, bonus, purchased } |
karma | { total, breakdown, rank } |
isAdmin | Platform admin flag |
useUsers()
Get all users who have accessed this room, with their roles.
const { users, setRole, refresh } = useUsers()
| Field | Description |
|---|
users | Array of RoomUser objects |
setRole(userId, role) | Change a user’s role in this room |
refresh() | Refetch the user list |
useUserLookup()
O(1) lookup helpers — useful when you have a userId and need to display a name or avatar.
const { getUser, getName, getEmail, userMap } = useUserLookup()
const name = getName(record.data.userId) // "Jane Smith"
Team Hooks
useTeams()
Create and manage teams with smart member addition.
const { teams, create, addMember, removeMember, deleteTeam } = useTeams()
// Create a team
const teamId = create('Engineering', { isOpen: false })
// Add by email (auto-invites if user not found)
await addMember(teamId, { email: 'jane@example.com' }, {
sendEmail: true,
miniappId: 'my-app',
teamName: 'Engineering',
})
// Add by userId directly
await addMember(teamId, existingUserId, 'member')
addMember is smart about how you identify people:
- By
userId: Direct add (user must be in the room)
- By
email: Looks up the user. If found, adds them. If not, creates a pending invite.
- By
username: Same lookup-then-invite logic as email.
Messaging Hooks
These hooks provide a complete messaging system out of the box — channels, messages, reactions, and read receipts.
useMessages(channelId, options?)
Subscribe to messages in a channel.
const { messages, status, send, edit, remove } = useMessages(channelId)
await send('Hello world!')
await send('Reply to thread', parentMessageId)
await edit(messageId, 'Updated text')
await remove(messageId)
useChannels()
Manage channels.
const { channels, create, archive, update, remove } = useChannels()
await create({ name: 'general', type: 'public' })
useChannelMembers(channelId)
Channel membership.
const { members, join, leave, isMember } = useChannelMembers(channelId)
useReactions(channelId)
Emoji reactions with grouping.
const { getReactionsForMessage, toggle } = useReactions(channelId)
// Toggle a reaction
await toggle(messageId, '👍')
// Get grouped reactions for display
const reactions = getReactionsForMessage(messageId)
// [{ emoji: '👍', count: 3, users: [...], currentUserReacted: true }]
useReadReceipts()
Track read state per channel.
const { markAsRead, getUnreadCount } = useReadReceipts()
await markAsRead(channelId)
const unread = getUnreadCount(channelId, messages)
Collaborative Editing Hooks
useYjsText(collection, recordId, fieldName)
Real-time collaborative text editing. Multiple users can edit the same text simultaneously.
const { text, setText, synced, canWrite } = useYjsText('documents', docId, 'content')
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
disabled={!synced || !canWrite}
/>
| Field | Description |
|---|
text | Current text value |
setText(value) | Update the text |
synced | true when initial sync with server is complete |
canWrite | true if user has write permission |
useYjsField(collection, recordId, fieldName)
Low-level Yjs document access for advanced use cases (rich text editors, custom CRDT structures, etc.).
const { doc, synced, canWrite, updateCount } = useYjsField('documents', docId, 'content')
// Access Yjs types directly
const yText = doc.getText('content')
const yMap = doc.getMap('metadata')
File Storage Hook
useR2Files(options?)
Upload, list, download, and delete files stored in Cloudflare R2.
const files = useR2Files({ scope: 'self' })
// Upload
const result = await files.upload(fileBlob, 'photo.png')
// Upload base64
await files.uploadBase64(base64String, 'image.png', 'image/png')
// List
const allFiles = await files.list()
// Get URL
const url = files.getUrl(allFiles[0]) // Public URL
// Download
await files.downloadFile(allFiles[0], 'download.png')
// Delete
await files.deleteFile(allFiles[0])
| Option | Values | Description |
|---|
scope | 'self' (default), 'user', 'widget' | File namespace |
widgetId | string | Required when scope is 'widget' |
| Field | Description |
|---|
upload(file, name?) | Upload a File or Blob |
uploadBase64(data, name, mime?) | Upload a base64-encoded string |
list(prefix?) | List files, optionally filtered by prefix |
getUrl(file) | Get the public URL for a file |
downloadFile(file, name?) | Trigger a browser download |
deleteFile(file) | Delete a file |
isUploading | true while an upload is in progress |
Legacy Hooks
These are available for backward compatibility with older widgets. New widgets should use useQuery / useMutations instead.
useStorage<T>(key, defaultValue, options?)
Persistent key-value storage with real-time sync. Works like useState but persists across sessions.
const [count, setCount] = useStorage('counter', 0)
const [notes, setNotes] = useStorage('my-notes', '', { scope: 'user' })
| Option | Values | Description |
|---|
scope | 'global', 'user' | 'global' = shared, 'user' = private |
useFiles(basePath, options?)
File-system-like API built on Yjs.
const files = useFiles('documents/')
files.write('readme.txt', 'Hello world')
const content = files.read('readme.txt')
const allFiles = files.list()