Marketplace
liveblocks
Builds real-time collaborative features with Liveblocks including presence, cursors, storage, comments, and notifications. Use when adding multiplayer experiences, collaborative editing, or live cursors to React applications.
$ Installieren
git clone https://github.com/mgd34msu/goodvibes-plugin /tmp/goodvibes-plugin && cp -r /tmp/goodvibes-plugin/plugins/goodvibes/skills/webdev/skills/liveblocks ~/.claude/skills/goodvibes-plugin// tip: Run this command in your terminal to install the skill
SKILL.md
name: liveblocks description: Builds real-time collaborative features with Liveblocks including presence, cursors, storage, comments, and notifications. Use when adding multiplayer experiences, collaborative editing, or live cursors to React applications.
Liveblocks
Complete toolkit for adding real-time collaboration to your app. Includes presence, storage, comments, notifications, and Yjs/Redux integration.
Quick Start
npm install @liveblocks/client @liveblocks/react
Configuration
// liveblocks.config.ts
import { createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";
const client = createClient({
publicApiKey: "pk_xxx", // or authEndpoint for production
});
// Define your types
type Presence = {
cursor: { x: number; y: number } | null;
name: string;
};
type Storage = {
todos: LiveList<{ id: string; text: string; done: boolean }>;
};
type UserMeta = {
id: string;
info: { name: string; avatar: string };
};
export const {
RoomProvider,
useMyPresence,
useUpdateMyPresence,
useOthers,
useSelf,
useStorage,
useMutation,
useRoom,
} = createRoomContext<Presence, Storage, UserMeta>(client);
Basic Usage
import { RoomProvider } from "./liveblocks.config";
function App() {
return (
<RoomProvider
id="my-room"
initialPresence={{ cursor: null, name: "Anonymous" }}
>
<CollaborativeApp />
</RoomProvider>
);
}
Presence
Track ephemeral user state (cursors, selections, typing indicators).
useMyPresence / useUpdateMyPresence
function Cursors() {
const updateMyPresence = useUpdateMyPresence();
return (
<div
onPointerMove={(e) => {
updateMyPresence({
cursor: { x: e.clientX, y: e.clientY }
});
}}
onPointerLeave={() => {
updateMyPresence({ cursor: null });
}}
>
<OthersCursors />
</div>
);
}
useOthers
function OthersCursors() {
const others = useOthers();
return (
<>
{others.map(({ connectionId, presence, info }) => {
if (!presence.cursor) return null;
return (
<Cursor
key={connectionId}
x={presence.cursor.x}
y={presence.cursor.y}
name={info?.name}
/>
);
})}
</>
);
}
useSelf
function UserInfo() {
const self = useSelf();
if (!self) return null;
return (
<div>
<span>You: {self.info?.name}</span>
<span>Connection: {self.connectionId}</span>
</div>
);
}
Selector Pattern (Performance)
// Only re-render when count changes
const count = useOthers((others) => others.length);
// Only get cursor positions
const cursors = useOthers((others) =>
others.map((other) => ({
id: other.connectionId,
cursor: other.presence.cursor
}))
);
Storage
Persist and sync data across clients using conflict-free data types.
Initialize Storage
<RoomProvider
id="my-room"
initialPresence={{ cursor: null }}
initialStorage={{
todos: new LiveList([]),
canvas: new LiveMap(),
settings: new LiveObject({ theme: "dark" })
}}
>
useStorage
function TodoList() {
const todos = useStorage((root) => root.todos);
if (todos === null) {
return <div>Loading...</div>;
}
return (
<ul>
{todos.map((todo, index) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
useMutation
Modify storage safely with mutations.
function TodoList() {
const todos = useStorage((root) => root.todos);
const addTodo = useMutation(({ storage }, text: string) => {
const todos = storage.get("todos");
todos.push({
id: crypto.randomUUID(),
text,
done: false
});
}, []);
const toggleTodo = useMutation(({ storage }, index: number) => {
const todos = storage.get("todos");
const todo = todos.get(index);
if (todo) {
todo.done = !todo.done;
}
}, []);
const deleteTodo = useMutation(({ storage }, index: number) => {
storage.get("todos").delete(index);
}, []);
return (
<div>
<button onClick={() => addTodo("New task")}>Add</button>
<ul>
{todos?.map((todo, i) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(i)}
/>
{todo.text}
<button onClick={() => deleteTodo(i)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Live Data Types
LiveList
const addItem = useMutation(({ storage }) => {
const list = storage.get("items");
list.push({ id: "1", value: "new" }); // Add to end
list.insert({ id: "2", value: "at 0" }, 0); // Insert at index
list.move(0, 2); // Move item
list.delete(1); // Delete at index
list.clear(); // Remove all
}, []);
LiveMap
const updateMap = useMutation(({ storage }) => {
const map = storage.get("shapes");
map.set("shape-1", { x: 100, y: 200 }); // Set value
map.get("shape-1"); // Get value
map.delete("shape-1"); // Delete key
map.has("shape-1"); // Check existence
}, []);
LiveObject
const updateObject = useMutation(({ storage }) => {
const settings = storage.get("settings");
settings.set("theme", "light"); // Set property
settings.get("theme"); // Get property
settings.update({ theme: "dark", fontSize: 16 }); // Update multiple
}, []);
Suspense Support
import { ClientSideSuspense } from "@liveblocks/react";
import { useStorage } from "@liveblocks/react/suspense";
function App() {
return (
<RoomProvider id="room">
<ClientSideSuspense fallback={<Loading />}>
<Editor />
</ClientSideSuspense>
</RoomProvider>
);
}
function Editor() {
// No null check needed with suspense hooks
const todos = useStorage((root) => root.todos);
return <TodoList todos={todos} />;
}
Authentication
Auth Endpoint
// app/api/liveblocks-auth/route.ts
import { Liveblocks } from "@liveblocks/node";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
});
export async function POST(request: Request) {
const session = await getSession(); // Your auth
const { room } = await request.json();
const session = liveblocks.prepareSession(session.user.id, {
userInfo: {
name: session.user.name,
avatar: session.user.avatar,
},
});
// Grant access to room
session.allow(room, session.FULL_ACCESS);
const { body, status } = await session.authorize();
return new Response(body, { status });
}
Client Config
const client = createClient({
authEndpoint: "/api/liveblocks-auth",
});
Broadcast
Send transient messages without storing them.
function Chat() {
const room = useRoom();
const sendMessage = (text: string) => {
room.broadcastEvent({
type: "MESSAGE",
text,
});
};
useEffect(() => {
return room.subscribe("event", ({ event }) => {
if (event.type === "MESSAGE") {
console.log("Received:", event.text);
}
});
}, [room]);
return <button onClick={() => sendMessage("Hello!")}>Send</button>;
}
History / Undo-Redo
import { useHistory, useCanUndo, useCanRedo } from "@liveblocks/react";
function UndoRedo() {
const history = useHistory();
const canUndo = useCanUndo();
const canRedo = useCanRedo();
return (
<div>
<button onClick={history.undo} disabled={!canUndo}>
Undo
</button>
<button onClick={history.redo} disabled={!canRedo}>
Redo
</button>
</div>
);
}
// Batch changes for single undo step
const batchUpdate = useMutation(({ storage }) => {
storage.get("todos").push({ id: "1", text: "A" });
storage.get("todos").push({ id: "2", text: "B" });
// Both will undo together
}, []);
// Pause/resume history
history.pause();
// ... make changes that shouldn't be in history
history.resume();
Yjs Integration
Use Liveblocks as a Yjs provider for text editors.
npm install @liveblocks/yjs yjs
import { useRoom } from "./liveblocks.config";
import { LiveblocksYjsProvider } from "@liveblocks/yjs";
import * as Y from "yjs";
function Editor() {
const room = useRoom();
useEffect(() => {
const yDoc = new Y.Doc();
const yProvider = new LiveblocksYjsProvider(room, yDoc);
// Use with TipTap, Quill, Lexical, etc.
const editor = new YourEditor({
extensions: [
Collaboration.configure({ document: yDoc }),
CollaborationCursor.configure({ provider: yProvider }),
],
});
return () => {
yProvider.destroy();
yDoc.destroy();
};
}, [room]);
}
Comments
Add contextual comments to your app.
npm install @liveblocks/react-comments
import { Thread, Composer } from "@liveblocks/react-comments";
import { useThreads } from "@liveblocks/react/suspense";
function Comments() {
const { threads } = useThreads();
return (
<div>
{threads.map((thread) => (
<Thread key={thread.id} thread={thread} />
))}
<Composer />
</div>
);
}
Common Patterns
Multiplayer Cursors
function Cursors() {
const updateMyPresence = useUpdateMyPresence();
const others = useOthers((others) =>
others.filter((o) => o.presence.cursor !== null)
);
return (
<div
className="canvas"
onPointerMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
updateMyPresence({
cursor: {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
},
});
}}
onPointerLeave={() => updateMyPresence({ cursor: null })}
>
{others.map(({ connectionId, presence, info }) => (
<Cursor
key={connectionId}
x={presence.cursor!.x}
y={presence.cursor!.y}
name={info?.name}
/>
))}
</div>
);
}
function Cursor({ x, y, name }: { x: number; y: number; name?: string }) {
return (
<div
className="cursor"
style={{
position: "absolute",
left: x,
top: y,
pointerEvents: "none",
}}
>
<svg>...</svg>
{name && <span>{name}</span>}
</div>
);
}
Who's Here
function AvatarStack() {
const others = useOthers();
const self = useSelf();
return (
<div className="avatar-stack">
{self && <Avatar user={self.info} />}
{others.map(({ connectionId, info }) => (
<Avatar key={connectionId} user={info} />
))}
</div>
);
}
Live Selection
type Presence = {
selectedId: string | null;
};
function SelectableItems() {
const updateMyPresence = useUpdateMyPresence();
const othersSelections = useOthers((others) =>
others.map((o) => o.presence.selectedId).filter(Boolean)
);
const items = useStorage((root) => root.items);
return (
<div>
{items?.map((item) => (
<div
key={item.id}
onClick={() => updateMyPresence({ selectedId: item.id })}
className={othersSelections.includes(item.id) ? "selected-by-other" : ""}
>
{item.name}
</div>
))}
</div>
);
}
Best Practices
- Use selectors to avoid unnecessary re-renders
- Type your presence/storage for autocomplete and safety
- Use Suspense hooks for cleaner loading states
- Batch mutations for undo/redo grouping
- Use authEndpoint in production (not public API key)
- Debounce cursor updates for performance
Repository

mgd34msu
Author
mgd34msu/goodvibes-plugin/plugins/goodvibes/skills/webdev/skills/liveblocks
0
Stars
0
Forks
Updated9h ago
Added1w ago