Skip to main content
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,
})
OptionTypeDescription
whereRecord<string, any>Filter by field values
orderBystringField to sort by
orderDir'asc' | 'desc'Sort direction
limitnumberMaximum records to return
Returns:
FieldTypeDescription
recordsRecordData<T>[]Matching records. Each has .id, .data, .createdAt, .updatedAt
status'loading' | 'ready' | 'error'Subscription state
errorstring | undefinedError 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:
FunctionDescription
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:
FieldDescription
idUser ID
nameDisplay name
emailEmail address
imageUrlAvatar URL
roleRole in this room ('viewer', 'member', 'admin')
credits{ total, subscription, bonus, purchased }
karma{ total, breakdown, rank }
isAdminPlatform admin flag

useUsers()

Get all users who have accessed this room, with their roles.
const { users, setRole, refresh } = useUsers()
FieldDescription
usersArray 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}
/>
FieldDescription
textCurrent text value
setText(value)Update the text
syncedtrue when initial sync with server is complete
canWritetrue 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])
OptionValuesDescription
scope'self' (default), 'user', 'widget'File namespace
widgetIdstringRequired when scope is 'widget'
FieldDescription
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
isUploadingtrue 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' })
OptionValuesDescription
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()