Collaborative Todos
A shared todo list synced in real time across all connected clients. When one user adds, toggles, or deletes a todo, all other users see it instantly.
Demonstrates: named event pub/sub (todo:created, todo:toggled, todo:deleted), optimistic updates, echo prevention
Source: examples/socket-hub/src/examples/CollaborativeTodos/
How it works
Section titled “How it works”User A adds a todo → Optimistic: add to local state immediately → channel.publish('todo:created', todo) → All subscribers (except User A) receive todo:created event → User B, C... update their stateNo server required. All state sync happens via WebSocket pub/sub.
Client code
Section titled “Client code”import { useAerostack } from '@aerostack/react'
interface Todo { id: string text: string done: boolean createdBy: string createdAt: number}
export function CollaborativeTodos() { const { realtime } = useAerostack() const [todos, setTodos] = useState<Todo[]>([]) const [inputText, setInputText] = useState('')
const myId = useRef(`user-${Math.random().toString(36).slice(2, 8)}`) const channelRef = useRef(null)
useEffect(() => { const channel = realtime.channel('todos/shared-workspace') channelRef.current = channel
channel .on('todo:created', ({ data, userId }) => { // Skip our own events (we added optimistically) if (userId === myId.current) return setTodos(prev => { if (prev.find(t => t.id === data.id)) return prev return [...prev, data] }) }) .on('todo:toggled', ({ data, userId }) => { if (userId === myId.current) return setTodos(prev => prev.map(t => t.id === data.id ? { ...t, done: data.done } : t )) }) .on('todo:deleted', ({ data, userId }) => { if (userId === myId.current) return setTodos(prev => prev.filter(t => t.id !== data.id)) }) .subscribe()
return () => { channelRef.current = null channel.unsubscribe() } }, [realtime])
const addTodo = () => { if (!inputText.trim()) return
const todo: Todo = { id: `todo-${Date.now()}-${Math.random().toString(36).slice(2)}`, text: inputText, done: false, createdBy: myId.current, createdAt: Date.now(), }
// Optimistic update setTodos(prev => [...prev, todo]) setInputText('')
// Broadcast to peers (userId is automatically attached by the SDK) channelRef.current?.publish('todo:created', todo) }
const toggleTodo = (id: string) => { setTodos(prev => prev.map(t => { if (t.id !== id) return t const updated = { ...t, done: !t.done } channelRef.current?.publish('todo:toggled', { id, done: updated.done }) return updated })) }
const deleteTodo = (id: string) => { setTodos(prev => prev.filter(t => t.id !== id)) channelRef.current?.publish('todo:deleted', { id }) }
return ( <div className="max-w-lg mx-auto p-6"> <div className="flex items-center gap-2 mb-6 text-sm text-gray-400"> <span className="w-2 h-2 rounded-full bg-green-400" /> Synced in real time </div>
<form onSubmit={e => { e.preventDefault(); addTodo() }} className="flex gap-2 mb-6" > <input value={inputText} onChange={e => setInputText(e.target.value)} placeholder="Add a todo..." className="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-2 text-white" /> <button type="submit" className="bg-blue-600 text-white px-6 py-2 rounded-xl font-medium" > Add </button> </form>
<div className="space-y-2"> {todos.length === 0 && ( <p className="text-gray-500 text-center py-8"> No todos yet. Add one above, then open another tab to see real-time sync! </p> )} {todos.map(todo => ( <div key={todo.id} className="flex items-center gap-3 p-3 rounded-xl border border-gray-800 bg-gray-900/50 group" > <button onClick={() => toggleTodo(todo.id)} className={`w-5 h-5 rounded border flex items-center justify-center flex-shrink-0 ${ todo.done ? 'bg-green-500 border-green-500' : 'border-gray-600 hover:border-green-500' }`} > {todo.done && <span className="text-white text-xs">✓</span>} </button> <span className={`flex-1 text-sm ${todo.done ? 'line-through text-gray-500' : 'text-white'}`}> {todo.text} </span> <button onClick={() => deleteTodo(todo.id)} className="opacity-0 group-hover:opacity-100 text-gray-500 hover:text-red-400 text-xs transition-opacity" > Delete </button> </div> ))} </div>
{todos.length > 0 && ( <div className="mt-4 text-xs text-gray-600 text-center"> {todos.filter(t => t.done).length} of {todos.length} completed </div> )} </div> )}Try it
Section titled “Try it”Open http://localhost:5173 in two browser windows. Add a todo in one — watch it appear instantly in the other.