Common patterns
Practical integration patterns for building Cloudillo applications.
Infinite Scroll with Actions
Load actions (posts, comments) with cursor-based pagination.
import { useApi, useInfiniteScroll, LoadMoreTrigger } from '@cloudillo/react'
function Feed() {
const { api } = useApi()
const { items, isLoading, sentinelRef } = useInfiniteScroll({
fetchPage: async (cursor, limit) => {
const result = await api.actions.listPaginated({
type: 'POST',
cursor,
limit
})
return {
items: result.data,
nextCursor: result.cursorPagination?.nextCursor ?? null,
hasMore: result.cursorPagination?.hasMore ?? false
}
},
pageSize: 20
})
return (
<div className="feed">
{items.map(action => (
<PostCard key={action.actionId} action={action} />
))}
<LoadMoreTrigger ref={sentinelRef} isLoading={isLoading} />
</div>
)
}Collaborative Document Editor
Set up a collaborative editing session with CRDT sync.
import { useCloudilloEditor } from '@cloudillo/react'
import { useEffect, useRef } from 'react'
import Quill from 'quill'
import { QuillBinding } from 'y-quill'
function CollaborativeEditor() {
const { yDoc, provider, synced, ownerTag, fileId } = useCloudilloEditor('quillo')
const editorRef = useRef<HTMLDivElement>(null)
const quillRef = useRef<Quill | null>(null)
useEffect(() => {
if (!synced || !editorRef.current) return
// Initialize Quill editor
const quill = new Quill(editorRef.current, {
theme: 'snow',
modules: { toolbar: true }
})
quillRef.current = quill
// Bind to Yjs document
const yText = yDoc.getText('content')
const binding = new QuillBinding(yText, quill, provider.awareness)
return () => {
binding.destroy()
quill.disable()
}
}, [synced, yDoc, provider])
if (!synced) {
return <LoadingSpinner />
}
return (
<div>
<div className="editor-header">
<span>Editing: {fileId}</span>
<span>Owner: {ownerTag}</span>
</div>
<div ref={editorRef} />
</div>
)
}Toast Notifications
Show feedback for user actions.
import { useToast, ToastContainer, Button } from '@cloudillo/react'
// In your app root
function App() {
return (
<>
<ToastContainer position="bottom-right" />
<MainContent />
</>
)
}
// In any component
function SaveButton({ data }) {
const { api } = useApi()
const toast = useToast()
const handleSave = async () => {
try {
await api.files.update(data.fileId, data)
toast.success('Changes saved successfully')
} catch (err) {
if (err.code === 'E-AUTH-UNAUTH') {
toast.error('Session expired. Please log in again.')
} else {
toast.error('Failed to save changes')
}
}
}
return <Button onClick={handleSave}>Save</Button>
}File Upload with Progress
Upload files with variant generation.
import { useApi } from '@cloudillo/react'
import { useState } from 'react'
function ImageUpload({ onUploaded }) {
const { api } = useApi()
const [uploading, setUploading] = useState(false)
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setUploading(true)
try {
// Upload with preset for automatic variant generation
const result = await api.files.uploadBlob(
'gallery', // Preset: generates thumbnail + SD variants
file.name,
file,
file.type
)
onUploaded({
fileId: result.fileId,
thumbnailId: result.variantId // Thumbnail variant ID
})
} catch (err) {
console.error('Upload failed:', err)
} finally {
setUploading(false)
}
}
return (
<div>
<input
type="file"
accept="image/*"
onChange={handleFileSelect}
disabled={uploading}
/>
{uploading && <LoadingSpinner />}
</div>
)
}Real-Time Presence
Show who’s currently viewing/editing.
import { useCloudilloEditor, AvatarGroup, Avatar } from '@cloudillo/react'
import { useEffect, useState } from 'react'
function PresenceIndicator() {
const { provider } = useCloudilloEditor('my-app')
const [users, setUsers] = useState<Map<number, any>>(new Map())
useEffect(() => {
if (!provider) return
const updateUsers = () => {
setUsers(new Map(provider.awareness.getStates()))
}
provider.awareness.on('change', updateUsers)
updateUsers()
return () => {
provider.awareness.off('change', updateUsers)
}
}, [provider])
const otherUsers = Array.from(users.entries())
.filter(([clientId]) => clientId !== provider?.awareness.clientID)
.map(([, state]) => state.user)
return (
<AvatarGroup max={5}>
{otherUsers.map((user, i) => (
<Avatar key={i} name={user.name} src={user.profilePic} size="sm" />
))}
</AvatarGroup>
)
}Role-Based UI
Show different UI based on user roles.
import { useAuth } from '@cloudillo/react'
import { ROLE_LEVELS, CommunityRole } from '@cloudillo/types'
function hasRole(userRoles: string[] | undefined, requiredRole: CommunityRole): boolean {
if (!userRoles?.length) return false
const userLevel = Math.max(...userRoles.map(r => ROLE_LEVELS[r as CommunityRole] ?? 0))
return userLevel >= ROLE_LEVELS[requiredRole]
}
function CommunitySettings() {
const [auth] = useAuth()
const canModerate = hasRole(auth?.roles, 'moderator')
const canAdmin = hasRole(auth?.roles, 'leader')
return (
<div>
<h2>Community Settings</h2>
{/* Everyone can see */}
<GeneralSettings />
{/* Moderators and above */}
{canModerate && <ModerationPanel />}
{/* Leaders only */}
{canAdmin && <AdminPanel />}
</div>
)
}Optimistic Updates
Update UI immediately while syncing in background.
import { useApi, useToast } from '@cloudillo/react'
import { useState } from 'react'
function LikeButton({ actionId, initialLiked, initialCount }) {
const { api } = useApi()
const toast = useToast()
const [liked, setLiked] = useState(initialLiked)
const [count, setCount] = useState(initialCount)
const handleLike = async () => {
// Optimistic update
const wasLiked = liked
const oldCount = count
setLiked(!liked)
setCount(liked ? count - 1 : count + 1)
try {
await api.actions.addReaction(actionId, { type: 'LOVE' })
} catch (err) {
// Rollback on error
setLiked(wasLiked)
setCount(oldCount)
toast.error('Failed to update reaction')
}
}
return (
<Button variant={liked ? 'primary' : 'ghost'} onClick={handleLike}>
{liked ? '❤️' : '🤍'} {count}
</Button>
)
}Debounced Search
Search with debouncing to reduce API calls.
import { useApi, Input } from '@cloudillo/react'
import { useState, useEffect, useRef } from 'react'
function SearchBox({ onResults }) {
const { api } = useApi()
const [query, setQuery] = useState('')
const [loading, setLoading] = useState(false)
const debounceRef = useRef<NodeJS.Timeout>()
useEffect(() => {
if (debounceRef.current) {
clearTimeout(debounceRef.current)
}
if (!query.trim()) {
onResults([])
return
}
debounceRef.current = setTimeout(async () => {
setLoading(true)
try {
const results = await api.profiles.list({ q: query, limit: 10 })
onResults(results)
} finally {
setLoading(false)
}
}, 300)
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current)
}
}
}, [query, api, onResults])
return (
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search profiles..."
icon={loading ? <LoadingSpinner size="sm" /> : <SearchIcon />}
/>
)
}Connection Request Flow
Handle bi-directional connection requests.
import { useApi, useToast, Button, ProfileCard } from '@cloudillo/react'
function ConnectionButton({ profile }) {
const { api } = useApi()
const toast = useToast()
const handleConnect = async () => {
try {
await api.actions.create({
type: 'CONN',
subject: profile.idTag,
content: 'Would love to connect!'
})
toast.success('Connection request sent')
} catch (err) {
toast.error('Failed to send request')
}
}
const handleAccept = async (actionId: string) => {
try {
await api.actions.accept(actionId)
toast.success('Connection accepted')
} catch (err) {
toast.error('Failed to accept connection')
}
}
// Render based on connection state
if (profile.connected === true) {
return <Badge variant="success">Connected</Badge>
}
if (profile.connected === 'R') {
return <Badge variant="info">Request Pending</Badge>
}
return <Button onClick={handleConnect}>Connect</Button>
}Error Boundary Pattern
Handle errors gracefully in components.
import { Component, ReactNode } from 'react'
import { EmptyState, Button } from '@cloudillo/react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
error?: Error
}
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false }
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
handleRetry = () => {
this.setState({ hasError: false, error: undefined })
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<EmptyState
icon={<AlertIcon />}
title="Something went wrong"
description={this.state.error?.message}
action={<Button onClick={this.handleRetry}>Try Again</Button>}
/>
)
}
return this.props.children
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
)
}See Also
- @cloudillo/react - React library
- @cloudillo/core - Core SDK
- Components Reference - All components