@cloudillo/react
Overview
React hooks and components for integrating Cloudillo into React applications.
Installation
pnpm add @cloudillo/react @cloudillo/coreHooks
useAuth()
Access authentication state using Jotai atoms. Returns a tuple [auth, setAuth] for reading and updating auth state.
import { useAuth } from '@cloudillo/react'
function UserInfo() {
const [auth, setAuth] = useAuth()
if (!auth?.idTag) {
return <div>Not authenticated</div>
}
return (
<div>
<p>User: {auth.idTag}</p>
<p>Name: {auth.name}</p>
<p>Tenant: {auth.tnId}</p>
<p>Roles: {auth.roles?.join(', ')}</p>
{auth.profilePic && <img src={auth.profilePic} alt="Profile" />}
</div>
)
}Returns: [AuthState | undefined, SetAtom<AuthState>]
interface AuthState {
tnId: number // Tenant ID
idTag?: string // Identity tag (e.g., "alice.cloudillo.net")
name?: string // Display name
profilePic?: string // Profile picture ID
roles?: string[] // Community roles
token?: string // JWT access token
}Usage patterns:
// Destructure the tuple
const [auth, setAuth] = useAuth()
// Check if user is authenticated
if (!auth?.idTag) {
return <LoginPrompt />
}
// Check for specific role
if (auth?.roles?.includes('admin')) {
return <AdminPanel />
}
// Update auth state (typically done by useCloudillo)
setAuth({
tnId: 123,
idTag: 'alice.cloudillo.net',
token: 'jwt-token'
})useApi()
Get a type-safe API client with automatic caching per idTag/token combination.
import { useApi } from '@cloudillo/react'
import { useEffect, useState } from 'react'
function PostsList() {
const { api, authenticated, setIdTag } = useApi()
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!api) return
api.actions.list({ type: 'POST', limit: 20 })
.then(setPosts)
.finally(() => setLoading(false))
}, [api])
if (!api) return <div>No API client (no idTag)</div>
if (!authenticated) return <div>Please log in</div>
if (loading) return <div>Loading...</div>
return (
<div>
{posts.map(post => (
<div key={post.actionId}>{post.content}</div>
))}
</div>
)
}Returns:
interface ApiHook {
api: ApiClient | null // Type-safe API client (null if no idTag)
authenticated: boolean // Whether user has a token
setIdTag: (idTag: string) => void // Set idTag for login flow
}The API client is the same as returned by createApiClient() from @cloudillo/core.
api can be null
api is null until an idTag is available (either from auth state or set via setIdTag). Always check for null before using.
useCloudillo()
Unified hook for microfrontend app initialization. Handles shell communication, authentication, and document context.
import { useCloudillo } from '@cloudillo/react'
function MyApp() {
const { token, ownerTag, fileId, idTag, roles, access, displayName } = useCloudillo('my-app')
if (!token) return <div>Loading...</div>
return (
<div>
<p>Document: {fileId}</p>
<p>Owner: {ownerTag}</p>
<p>Access: {access}</p>
<p>User: {idTag}</p>
</div>
)
}This hook:
- Calls
getAppBus().init(appName)on mount - Parses
ownerTagandfileIdfromlocation.hash(format:#ownerTag:fileId) - Updates auth state via
useAuth() - Returns combined state from bus and URL
Returns:
interface UseCloudillo {
token?: string // Access token
ownerTag: string // Owner of the current document (from URL hash)
fileId?: string // Current file/document ID (from URL hash)
idTag?: string // Current user's identity tag
tnId?: number // Tenant ID
roles?: string[] // User's roles
access?: 'read' | 'write' // Access level to current resource
displayName?: string // User's display name (for anonymous guests)
}useCloudilloEditor()
Extended hook for CRDT-based collaborative document editing. Returns everything from useCloudillo() plus Yjs document and sync state.
import { useCloudilloEditor } from '@cloudillo/react'
function CollaborativeEditor() {
const { token, yDoc, provider, synced, ownerTag, fileId, access } = useCloudilloEditor('quillo')
if (!synced) return <LoadingSpinner />
// Use yDoc with your editor binding (Quill, TipTap, Monaco, etc.)
const yText = yDoc.getText('content')
return <QuillEditor yText={yText} provider={provider} />
}This hook:
- Calls
useCloudillo(appName)internally - Opens CRDT connection via
openYDoc()when token and docId are available - Listens for sync events and notifies shell via
bus.notifyReady('synced') - Cleans up WebSocket provider on unmount
Returns:
interface UseCloudilloEditor extends UseCloudillo {
yDoc: Y.Doc // Yjs document instance
provider?: WebsocketProvider // WebSocket sync provider
synced: boolean // Whether initial sync is complete
}useInfiniteScroll()
Cursor-based infinite scroll pagination with IntersectionObserver.
import { useInfiniteScroll } from '@cloudillo/react'
function FileList() {
const { api } = useApi()
const { items, isLoading, hasMore, sentinelRef, prepend, reset } = useInfiniteScroll({
fetchPage: async (cursor, limit) => {
const result = await api.files.listPaginated({ cursor, limit })
return {
items: result.data,
nextCursor: result.cursorPagination?.nextCursor ?? null,
hasMore: result.cursorPagination?.hasMore ?? false
}
},
pageSize: 30,
deps: [folderId, sortField] // Reset when these change
})
return (
<div>
{items.map(file => <FileCard key={file.fileId} file={file} />)}
{/* Sentinel triggers loadMore when visible */}
<div ref={sentinelRef} />
{isLoading && <LoadingSpinner />}
</div>
)
}Options:
interface UseInfiniteScrollOptions<T> {
fetchPage: (cursor: string | null, limit: number) => Promise<{
items: T[]
nextCursor: string | null
hasMore: boolean
}>
pageSize?: number // Default: 20
deps?: React.DependencyList // Reset when these change
enabled?: boolean // Default: true
}Returns:
interface UseInfiniteScrollReturn<T> {
items: T[] // All loaded items
isLoading: boolean // Initial load in progress
isLoadingMore: boolean // Loading more pages
error: Error | null // Last fetch error
hasMore: boolean // More items available
loadMore: () => void // Manually load next page
reset: () => void // Reset and reload
prepend: (items: T[]) => void // Add items to start (real-time)
sentinelRef: RefObject<HTMLDivElement> // Attach to trigger element
}Common Patterns
Pattern 1: Microfrontend App
The typical pattern for a Cloudillo microfrontend app:
import { useCloudillo, useApi } from '@cloudillo/react'
import { useEffect, useState } from 'react'
function App() {
const { token, idTag, ownerTag, fileId, access } = useCloudillo('my-app')
const { api } = useApi()
const [data, setData] = useState(null)
useEffect(() => {
if (!api || !fileId) return
api.files.getDescriptor(fileId)
.then(setData)
.catch(console.error)
}, [api, fileId])
if (!token) return <div>Initializing...</div>
if (!data) return <div>Loading...</div>
return (
<div>
<h1>{data.fileName}</h1>
<p>Owner: {ownerTag}</p>
<p>Access: {access}</p>
</div>
)
}Pattern 2: Collaborative Editor
import { useCloudilloEditor } from '@cloudillo/react'
import { useEffect } from 'react'
function Editor() {
const { yDoc, provider, synced, access } = useCloudilloEditor('my-editor')
useEffect(() => {
if (!synced) return
const yText = yDoc.getText('content')
// Set up your editor binding here
// e.g., QuillBinding, TipTapExtension, etc.
return () => {
// Clean up binding
}
}, [yDoc, synced])
if (!synced) return <div>Syncing document...</div>
return (
<div>
{access === 'read' && <div className="read-only-banner">Read only</div>}
<div id="editor-container" />
</div>
)
}Pattern 3: Fetching Data with useApi
import { useApi } from '@cloudillo/react'
import { useEffect, useState } from 'react'
function Profile({ idTag }) {
const { api } = useApi()
const [profile, setProfile] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
if (!api) return
api.profiles.get(idTag)
.then(setProfile)
.catch(setError)
}, [api, idTag])
if (!api) return <div>No API client</div>
if (error) return <div>Error: {error.message}</div>
if (!profile) return <div>Loading...</div>
return (
<div>
<h1>{profile.name}</h1>
<p>{profile.idTag}</p>
</div>
)
}Pattern 4: Creating Actions
import { useApi } from '@cloudillo/react'
import { useState } from 'react'
function CreatePost() {
const { api, authenticated } = useApi()
const [text, setText] = useState('')
const [posting, setPosting] = useState(false)
if (!authenticated) return <div>Please log in to post</div>
const handleSubmit = async (e) => {
e.preventDefault()
if (!api || !text.trim()) return
setPosting(true)
try {
await api.actions.create({
type: 'POST',
content: { text }
})
setText('')
} catch (error) {
console.error('Failed to create post:', error)
} finally {
setPosting(false)
}
}
return (
<form onSubmit={handleSubmit}>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="What's on your mind?"
/>
<button type="submit" disabled={posting || !text.trim()}>
{posting ? 'Posting...' : 'Post'}
</button>
</form>
)
}Pattern 5: Role-Based Rendering
import { useAuth } from '@cloudillo/react'
function AdminPanel() {
const [auth] = useAuth()
if (!auth?.roles?.includes('admin')) {
return <div>Access denied. Admin role required.</div>
}
return (
<div>
<h1>Admin Panel</h1>
{/* Admin-only features */}
</div>
)
}Pattern 6: File Upload
import { useApi } from '@cloudillo/react'
import { useState } from 'react'
function ImageUpload() {
const { api } = useApi()
const [uploading, setUploading] = useState(false)
const [result, setResult] = useState(null)
const handleFileChange = async (e) => {
const file = e.target.files?.[0]
if (!file || !api) return
setUploading(true)
try {
// Upload the file
const uploaded = await api.files.uploadBlob(
'gallery', // preset
file.name, // fileName
file, // file data
file.type // contentType
)
setResult(uploaded)
} catch (error) {
console.error('Upload failed:', error)
} finally {
setUploading(false)
}
}
return (
<div>
<input type="file" accept="image/*" onChange={handleFileChange} />
{uploading && <div>Uploading...</div>}
{result && <div>Uploaded: {result.fileId}</div>}
</div>
)
}Pattern 7: Real-Time Updates with RTDB
import { useApi, useAuth } from '@cloudillo/react'
import { useEffect, useState } from 'react'
import { RtdbClient } from '@cloudillo/rtdb'
import { getRtdbUrl } from '@cloudillo/core'
function TodoList({ dbFileId }) {
const [auth] = useAuth()
const [todos, setTodos] = useState([])
useEffect(() => {
if (!auth?.token || !auth?.idTag) return
const rtdb = new RtdbClient({
dbId: dbFileId,
auth: { getToken: () => auth.token },
serverUrl: getRtdbUrl(auth.idTag, dbFileId, auth.token)
})
const todosRef = rtdb.collection('todos')
// Subscribe to real-time updates
const unsubscribe = todosRef.onSnapshot((snapshot) => {
setTodos(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })))
})
// Cleanup on unmount
return () => {
unsubscribe()
rtdb.disconnect()
}
}, [auth?.token, auth?.idTag, dbFileId])
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}TypeScript Support
All hooks are fully typed:
import type { AuthState, ApiHook } from '@cloudillo/react'
import { useAuth, useApi } from '@cloudillo/react'
function MyComponent() {
const [auth, setAuth] = useAuth()
const { api, authenticated, setIdTag }: ApiHook = useApi()
// TypeScript knows the types
auth?.idTag // string | undefined
auth?.tnId // number | undefined
auth?.roles // string[] | undefined
api?.profiles.getOwn() // Returns Promise<ProfileKeys>
}See Also
- @cloudillo/core - Core SDK
- @cloudillo/rtdb - Real-time database
- REST API - API reference