Complete guide to integrating Laravel Chorus with React for real-time, offline-capable applications.
// app.tsx
import { ChorusProvider } from '@/chorus/react';
export default function AppSidebarLayoutContent({ user }) {
return (
<ChorusProvider
userId={user.id}
channelPrefix={user.tenant_id}
>
{ children }
</ChorusProvider>
);
}
// components/MessagesList.tsx
import { useTable } from '@/chorus/react';
import type { Message} from "@/types";
export default function MessagesList() {
// Get data and actions from table hook
const {
data: messages,
isLoading,
error,
create,
update,
remove
} = useTable<Message>('messages');
const handleSendMessage = async (body: string) => {
const tempId = crypto.randomUUID();
await create(
// Optimistic data (immediate UI update)
{
id: tempId,
body,
user_id: user.id,
platform_id: currentPlatform.id,
created_at: new Date(),
updated_at: new Date(),
},
// Server data
{
body,
platform_id: currentPlatform.id,
}
);
};
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={() => remove(message.id)}>
Delete
</button>
</div>
))}
<MessageForm onSubmit={handleSendMessage} />
</div>
);
}
const {
data, // Current synchronized data
isLoading, // Initial loading state
error, // Error state
lastUpdate, // Last synchronization timestamp
create, // Create new records
update, // Update existing records
remove, // Delete records
} = useTable<T>('tableName');
import { useTable, useOffline } from '@/chorus/react';
interface User {
id: number;
name: string;
email: string;
is_active: boolean;
}
export default function UserList() {
const {
data: users,
isLoading,
error,
lastUpdate,
update
} = useTable<User>('users');
const { isOffline } = useOffline();
const toggleUserStatus = async (user: User) => {
await update(
user.id,
// Optimistic update
{ ...user, is_active: !user.is_active },
// Server update
{ is_active: !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>
);
}
import { useOffline } from '@/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>
);
}
import { useState } from 'react';
import { useTable } from '@/chorus/react';
interface Message {
id: string;
body: string;
updated_at: Date;
is_edited?: boolean;
}
export default function MessageEditor({ messageId }: { messageId: string }) {
const { data: messages, update } = 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;
const originalText = message.body;
try {
await update(
messageId,
// Optimistic update
{
...message,
body: editText,
updated_at: new Date(),
is_edited: true
},
// Server update
{ body: editText },
// Error callback - rollback on failure
(error) => {
console.error('Update failed:', error);
// The hook automatically handles rollback
setEditText(originalText);
alert('Failed to save changes. Please try again.');
}
);
setIsEditing(false);
} 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>
);
}
import { useTable, usePresence } from '@/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>
);
}
import { useRejectedHarmonics } from '@/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>
);
}