Setup
1. Add the Chorus Provider
Wrap your app’s layout file with the Chorus provider:Copy
// app.tsx
import { ChorusProvider } from '@pixelsprout/chorus-react';
export default function AppSidebarLayoutContent({ user }) {
return (
<ChorusProvider
userId={user.id}
channelPrefix={user.tenant_id}
>
{ children }
</ChorusProvider>
);
}
2. Use Data and Actions in Components
Use Chorus hooks for data access and generated action functions for writes:Copy
// components/MessagesList.tsx
import { useTable } from '@pixelsprout/chorus-react';
import { createMessageWithActivityAction, deleteMessageAction } from '@/_generated/chorus-actions';
import { uuidv7 } from 'uuidv7';
import type { Message } from "@/types";
export default function MessagesList() {
// Get synchronized data
const {
data: messages,
isLoading,
error
} = useTable<Message>('messages');
const handleSendMessage = async (body: string) => {
const messageId = uuidv7();
const result = await createMessageWithActivityAction((writes) => {
// Create the message
writes.messages.create({
id: messageId,
body,
user_id: user.id,
platform_id: currentPlatform.id,
tenant_id: user.tenant_id,
});
// Update user activity (automatic)
writes.users.update({
id: user.id,
last_activity_at: new Date().toISOString(),
});
// Update platform metrics (automatic)
writes.platforms.update({
id: currentPlatform.id,
last_message_at: new Date().toISOString(),
});
});
if (result.success) {
console.log('Message created successfully');
}
};
const handleDeleteMessage = async (messageId: string) => {
await deleteMessageAction((writes) => {
writes.messages.delete(messageId);
});
};
if (isLoading) return <div>Loading messages...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div className="messages">
{messages?.map((message) => (
<div key={message.id} className="message">
<p>{message.body}</p>
<small>{new Date(message.created_at).toLocaleTimeString()}</small>
<button onClick={() => handleDeleteMessage(message.id)}>
Delete
</button>
</div>
))}
<MessageForm onSubmit={handleSendMessage} />
</div>
);
}
Core Hooks
useTable Hook
The primary hook provides data access for synchronized tables:Copy
const {
data, // Current synchronized data
isLoading, // Initial loading state
error, // Error state
lastUpdate, // Last synchronization timestamp
} = useTable<T>('tableName');
Chorus Actions for Writes
Use generated action functions for all write operations:Copy
import { useTable, useOffline } from '@pixelsprout/chorus-react';
import { updateUserStatusAction } from '@/_generated/chorus-actions';
interface User {
id: number;
name: string;
email: string;
is_active: boolean;
}
export default function UserList() {
const {
data: users,
isLoading,
error,
lastUpdate
} = useTable<User>('users');
const { isOffline } = useOffline();
const toggleUserStatus = async (user: User) => {
await updateUserStatusAction((writes) => {
writes.users.update({
id: user.id,
is_active: !user.is_active,
});
// Track admin activity
writes.admin_logs.create({
action: 'user_status_changed',
user_id: user.id,
performed_by: auth.user.id,
details: {
old_status: user.is_active,
new_status: !user.is_active
},
});
});
};
return (
<div>
{isOffline && (
<div className="offline-banner">
You're offline. Changes will sync when reconnected.
</div>
)}
{lastUpdate && (
<div className="sync-status">
Last updated: {lastUpdate.toLocaleTimeString()}
</div>
)}
{isLoading ? (
<div>Loading users...</div>
) : error ? (
<div>Error: {error}</div>
) : (
<div className="user-list">
{users?.map((user) => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => toggleUserStatus(user)}>
{user.is_active ? 'Deactivate' : 'Activate'}
</button>
</div>
))}
</div>
)}
</div>
);
}
useOffline Hook
Monitor and handle offline state:Copy
import { useOffline } from '@pixelsprout/chorus-react';
export default function OfflineIndicator() {
const {
isOffline,
isOnline,
queuedOperations,
clearQueue
} = useOffline();
return (
<div className={`status-bar ${isOffline ? 'offline' : 'online'}`}>
{isOffline ? (
<div className="offline-status">
<span>📡 Offline</span>
{queuedOperations.length > 0 && (
<span>
{queuedOperations.length} operations queued
</span>
)}
</div>
) : (
<div className="online-status">
<span>🟢 Online</span>
</div>
)}
{queuedOperations.length > 0 && (
<button onClick={clearQueue} className="clear-queue">
Clear Queue
</button>
)}
</div>
);
}
Error Handling and Optimistic Updates
Chorus Actions automatically handle optimistic updates with rollback:Copy
import { useState } from 'react';
import { useTable } from '@pixelsprout/chorus-react';
import { updateMessageAction } from '@/_generated/chorus-actions';
interface Message {
id: string;
body: string;
updated_at: Date;
is_edited?: boolean;
}
export default function MessageEditor({ messageId }: { messageId: string }) {
const { data: messages } = useTable<Message>('messages');
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState('');
const message = messages?.find(m => m.id === messageId);
const handleSave = async () => {
if (!message || !editText.trim()) return;
try {
const result = await updateMessageAction((writes) => {
writes.messages.update({
id: messageId,
body: editText,
is_edited: true,
edited_at: new Date().toISOString(),
});
// Track edit activity
writes.users.update({
id: auth.user.id,
last_activity_at: new Date().toISOString(),
});
});
if (result.success) {
setIsEditing(false);
} else {
console.error('Update failed:', result.error);
alert('Failed to save changes. Please try again.');
}
} catch (error) {
console.error('Update error:', error);
}
};
if (!message) return null;
return (
<div className="message-editor">
{isEditing ? (
<div>
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
className="edit-textarea"
/>
<div className="edit-actions">
<button onClick={handleSave}>Save</button>
<button onClick={() => {
setIsEditing(false);
setEditText('');
}}>
Cancel
</button>
</div>
</div>
) : (
<div>
<p>{message.body}</p>
{message.is_edited && (
<small className="edited-indicator">(edited)</small>
)}
<button onClick={() => {
setIsEditing(true);
setEditText(message.body);
}}>
Edit
</button>
</div>
)}
</div>
);
}
Real-time Collaboration
Build collaborative features with presence indicators:Copy
import { useTable, usePresence } from '@pixelsprout/chorus-react';
import { useState, useEffect } from 'react';
interface Message {
id: string;
body: string;
}
export default function CollaborativeEditor() {
const { data: messages, update } = useTable<Message>('messages');
const [editingUsers, setEditingUsers] = useState<Set<number>>(new Set());
// Track who's currently editing
const { broadcast, subscribe } = usePresence('message-editor');
const handleStartEditing = (messageId: string) => {
broadcast('start-editing', { messageId, userId: user.id });
};
const handleStopEditing = (messageId: string) => {
broadcast('stop-editing', { messageId, userId: user.id });
};
useEffect(() => {
const unsubscribe = subscribe((event, data) => {
if (event === 'start-editing') {
setEditingUsers(prev => new Set([...prev, data.userId]));
} else if (event === 'stop-editing') {
setEditingUsers(prev => {
const newSet = new Set(prev);
newSet.delete(data.userId);
return newSet;
});
}
});
return unsubscribe;
}, [subscribe]);
return (
<div className="collaborative-editor">
{messages?.map(message => (
<div key={message.id} className="message-item">
<div className="message-content">
{message.body}
</div>
{editingUsers.size > 0 && (
<div className="editing-indicators">
{Array.from(editingUsers).map(userId => (
<div key={userId} className="editing-user">
👤 User {userId} is editing...
</div>
))}
</div>
)}
</div>
))}
</div>
);
}
Error Handling
Rejected Operations Handling
Handle and display rejected operations:Copy
import { useRejectedHarmonics } from '@pixelsprout/chorus-react';
export default function RejectedOperations() {
const {
rejectedOperations,
retry,
dismiss,
dismissAll
} = useRejectedHarmonics();
if (rejectedOperations.length === 0) return null;
return (
<div className="rejected-operations">
<h3>Failed Operations</h3>
{rejectedOperations.map((operation) => (
<div key={operation.id} className="rejected-operation">
<div className="operation-details">
<strong>{operation.action}</strong> on {operation.table}
<p>{operation.error}</p>
</div>
<div className="operation-actions">
<button onClick={() => retry(operation.id)}>
Retry
</button>
<button onClick={() => dismiss(operation.id)}>
Dismiss
</button>
</div>
</div>
))}
<button onClick={dismissAll} className="dismiss-all">
Dismiss All
</button>
</div>
);
}
Next Steps
Vue Integration
Learn about Vue.js integration (Coming Soon)
Chorus Actions
Master server-side Chorus Actions
Concepts Deep Dive
Understand Chorus core concepts
Advanced Features
Explore advanced configuration options
You now have everything you need to build powerful real-time React applications with Laravel Chorus. The combination of optimistic updates, offline support, and real-time synchronization will deliver an exceptional user experience.