@cloudillo/react
@cloudillo/react
React hooks and components for integrating Cloudillo into React applications.
Installation
pnpm add @cloudillo/react @cloudillo/base @cloudillo/typesComponents
CloudilloProvider
Context provider that manages authentication state and API client for your entire app.
import { CloudilloProvider } from '@cloudillo/react'
function App() {
return (
<CloudilloProvider appName="my-app">
<YourApp />
</CloudilloProvider>
)
}Props:
interface CloudilloProviderProps {
appName: string // Your app name for initialization
children: React.ReactNode
baseUrl?: string // Optional: API base URL
onAuthChange?: (auth: AuthState) => void // Optional: Auth state callback
}Example with options:
<CloudilloProvider
appName="my-app"
baseUrl="https://api.cloudillo.com"
onAuthChange={(auth) => {
console.log('Auth changed:', auth)
}}
>
<YourApp />
</CloudilloProvider>MicrofrontendContainer
Component for loading microfrontend apps as iframes with postMessage communication.
import { MicrofrontendContainer } from '@cloudillo/react'
function AppShell() {
return (
<div className="app-container">
<MicrofrontendContainer
appName="quillo"
src="/apps/quillo/index.html"
docId="document-123"
/>
</div>
)
}Props:
interface MicrofrontendContainerProps {
appName: string // Name of the app
src: string // URL to load in iframe
docId?: string // Optional: Document ID to pass to app
className?: string // Optional: CSS class
style?: React.CSSProperties // Optional: Inline styles
onLoad?: () => void // Optional: Called when iframe loads
}How it works:
- Loads the app in an iframe
- Sends init message with auth credentials via postMessage
- Handles bidirectional communication
- Provides theme and dark mode info
ProfileDropdown
Pre-built user profile dropdown component.
import { ProfileDropdown } from '@cloudillo/react'
function Header() {
return (
<header>
<h1>My App</h1>
<ProfileDropdown />
</header>
)
}Features:
- Displays user profile picture and name
- Shows dropdown menu on click
- Links to profile, settings, logout
- Customizable via className
Props:
interface ProfileDropdownProps {
className?: string // Optional: CSS class
}Hooks
useAuth()
Access authentication state from anywhere in your app.
import { useAuth } from '@cloudillo/react'
function UserInfo() {
const auth = useAuth()
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:
interface AuthState {
tnId: number // Tenant ID
idTag?: string // User identity (e.g., "alice@example.com")
name?: string // Display name
profilePic?: string // Profile picture URL
roles?: string[] // User roles
settings?: Record<string, unknown> // User settings
token?: string // Access token
}Usage patterns:
// Check if user is authenticated
const auth = useAuth()
if (!auth.idTag) {
return <LoginPrompt />
}
// Check for specific role
if (auth.roles?.includes('admin')) {
return <AdminPanel />
}
// Use profile picture
{auth.profilePic && <img src={auth.profilePic} />}useApi()
Get the API client instance.
import { useApi } from '@cloudillo/react'
import { useEffect, useState } from 'react'
function PostsList() {
const api = useApi()
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
api.action.get({ type: 'POST', _limit: 20 })
.then(setPosts)
.finally(() => setLoading(false))
}, [api])
if (loading) return <div>Loading...</div>
return (
<div>
{posts.map(post => (
<div key={post.actionId}>{post.content.text}</div>
))}
</div>
)
}Returns:
const api: ApiClientThe API client is the same as returned by createApiClient() from @cloudillo/base.
Utility Functions
mergeClasses(…classes: (string | undefined | null | false)[]): string
Utility for conditional CSS class names.
import { mergeClasses } from '@cloudillo/react'
function Button({ primary, disabled, className }) {
return (
<button
className={mergeClasses(
'btn',
primary && 'btn-primary',
disabled && 'btn-disabled',
className
)}
>
Click me
</button>
)
}Common Patterns
Pattern 1: Fetching Data
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(() => {
api.profile.get({ idTag })
.then(setProfile)
.catch(setError)
}, [api, idTag])
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 2: Creating Actions
import { useApi } from '@cloudillo/react'
import { useState } from 'react'
function CreatePost() {
const api = useApi()
const [text, setText] = useState('')
const [posting, setPosting] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
setPosting(true)
try {
await api.action.post({
type: 'POST',
content: { text }
})
setText('') // Clear form
alert('Post created!')
} catch (error) {
alert('Failed to create post')
} 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}>
{posting ? 'Posting...' : 'Post'}
</button>
</form>
)
}Pattern 3: 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 4: Real-Time Updates
import { useApi } from '@cloudillo/react'
import { useEffect, useState } from 'react'
import { RtdbClient } from '@cloudillo/rtdb'
function TodoList() {
const api = useApi()
const [todos, setTodos] = useState([])
useEffect(() => {
const rtdb = new RtdbClient({
fileId: 'my-todos',
token: api.token,
url: 'wss://server.com/ws/rtdb'
})
const todosRef = rtdb.collection('todos')
// Subscribe to real-time updates
const unsubscribe = todosRef.onSnapshot((snapshot) => {
setTodos(snapshot)
})
// Cleanup on unmount
return () => unsubscribe()
}, [api])
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}Pattern 5: File Upload
import { useApi } from '@cloudillo/react'
import { useState } from 'react'
function ImageUpload() {
const api = useApi()
const [uploading, setUploading] = useState(false)
const [imageUrl, setImageUrl] = useState(null)
const handleFileChange = async (e) => {
const file = e.target.files[0]
if (!file) return
setUploading(true)
try {
// 1. Create file metadata
const fileMetadata = await api.file.post({
fileTp: 'BLOB',
contentType: file.type
})
// 2. Upload binary data
const formData = new FormData()
formData.append('file', file)
formData.append('fileId', fileMetadata.fileId)
await api.file.upload.post(formData)
// 3. Get the file URL
setImageUrl(`/file/${fileMetadata.fileId}`)
} catch (error) {
alert('Upload failed: ' + error.message)
} finally {
setUploading(false)
}
}
return (
<div>
<input type="file" accept="image/*" onChange={handleFileChange} />
{uploading && <div>Uploading...</div>}
{imageUrl && <img src={imageUrl} alt="Uploaded" />}
</div>
)
}Pattern 6: Dark Mode
import { useAuth } from '@cloudillo/react'
import { useEffect } from 'react'
function ThemeProvider({ children }) {
const auth = useAuth()
useEffect(() => {
// Apply dark mode class to body
if (auth.settings?.darkMode) {
document.body.classList.add('dark')
} else {
document.body.classList.remove('dark')
}
}, [auth.settings?.darkMode])
return <>{children}</>
}Complete Example App
Here’s a complete example of a simple post viewer:
import React, { useEffect, useState } from 'react'
import { CloudilloProvider, useAuth, useApi } from '@cloudillo/react'
function App() {
return (
<CloudilloProvider appName="posts-viewer">
<PostsApp />
</CloudilloProvider>
)
}
function PostsApp() {
const auth = useAuth()
if (!auth.idTag) {
return <div>Loading...</div>
}
return (
<div className="app">
<Header />
<PostsList />
</div>
)
}
function Header() {
const auth = useAuth()
return (
<header>
<h1>Posts</h1>
<div className="user-info">
{auth.profilePic && (
<img src={auth.profilePic} alt={auth.name} />
)}
<span>{auth.name}</span>
</div>
</header>
)
}
function PostsList() {
const api = useApi()
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
api.action.get({ type: 'POST', status: 'A', _limit: 50 })
.then(data => setPosts(data.data || []))
.catch(error => console.error('Failed to load posts:', error))
.finally(() => setLoading(false))
}, [api])
if (loading) return <div>Loading posts...</div>
return (
<div className="posts-list">
{posts.map(post => (
<Post key={post.actionId} post={post} />
))}
</div>
)
}
function Post({ post }) {
return (
<article className="post">
<div className="post-header">
{post.issuer?.profilePic && (
<img src={post.issuer.profilePic} alt={post.issuer.name} />
)}
<div>
<strong>{post.issuer?.name}</strong>
<span>{post.issuer?.idTag}</span>
</div>
</div>
<div className="post-content">
<p>{post.content?.text}</p>
</div>
<div className="post-stats">
{post.stat?.reactions > 0 && (
<span>{post.stat.reactions} reactions</span>
)}
{post.stat?.comments > 0 && (
<span>{post.stat.comments} comments</span>
)}
</div>
</article>
)
}
export default AppTypeScript Support
All hooks and components are fully typed:
import type { AuthState } from '@cloudillo/react'
import { useAuth } from '@cloudillo/react'
function MyComponent() {
const auth: AuthState = useAuth()
// TypeScript knows the shape of auth
auth.idTag // string | undefined
auth.tnId // number
auth.roles // string[] | undefined
}Testing
Mocking CloudilloProvider
import { render } from '@testing-library/react'
import { CloudilloProvider } from '@cloudillo/react'
const mockAuth = {
tnId: 123,
idTag: 'test@example.com',
name: 'Test User',
roles: ['user']
}
function TestWrapper({ children }) {
return (
<CloudilloProvider appName="test-app">
{children}
</CloudilloProvider>
)
}
test('renders user info', () => {
render(<MyComponent />, { wrapper: TestWrapper })
// Test your component
})See Also
- @cloudillo/base - Core SDK
- Getting Started - Build your first app
- REST API - API reference
- Microfrontends - Build shell-integrated apps