Skip to main content
Back to Blog
Tutorials
3 min read
January 4, 2025

How to Build a Real-Time Data Sync System with Liveblocks in React

Implement real-time collaboration in React using Liveblocks with presence, conflict-free data structures, cursors, and live editing.

Ryel Banfield

Founder & Lead Developer

Liveblocks makes it easy to add real-time collaboration to any React app. Here is how to build it.

Install

pnpm add @liveblocks/client @liveblocks/react @liveblocks/react-ui

Configure Liveblocks

// liveblocks.config.ts
import { createClient } from "@liveblocks/client";
import { createRoomContext, createLiveblocksContext } from "@liveblocks/react";

const client = createClient({
  publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY!,
  // Or use authEndpoint for authenticated users:
  // authEndpoint: "/api/liveblocks-auth",
});

// Define your collaborative types
type Presence = {
  cursor: { x: number; y: number } | null;
  name: string;
  color: string;
};

type Storage = {
  todos: LiveList<LiveObject<{
    id: string;
    text: string;
    completed: boolean;
    createdBy: string;
  }>>;
  canvasObjects: LiveMap<string, LiveObject<{
    x: number;
    y: number;
    width: number;
    height: number;
    color: string;
  }>>;
};

type UserMeta = {
  id: string;
  info: {
    name: string;
    avatar: string;
    color: string;
  };
};

type RoomEvent = {
  type: "NOTIFICATION";
  message: string;
};

export const {
  RoomProvider,
  useMyPresence,
  useOthers,
  useStorage,
  useMutation,
  useSelf,
  useRoom,
  useBroadcastEvent,
  useEventListener,
} = createRoomContext<Presence, Storage, UserMeta, RoomEvent>(client);

export const { LiveblocksProvider } = createLiveblocksContext(client);

// Re-export types from @liveblocks/client for use in storage
import { LiveList, LiveMap, LiveObject } from "@liveblocks/client";
export { LiveList, LiveMap, LiveObject };

Auth Endpoint

// app/api/liveblocks-auth/route.ts
import { Liveblocks } from "@liveblocks/node";
import { NextRequest, NextResponse } from "next/server";

const liveblocks = new Liveblocks({
  secret: process.env.LIVEBLOCKS_SECRET_KEY!,
});

export async function POST(request: NextRequest) {
  // Get the current user from your auth system
  // const user = await getCurrentUser();
  const user = { id: "user-1", name: "Demo User", avatar: "/avatars/default.png" };

  const colors = ["#ef4444", "#3b82f6", "#22c55e", "#eab308", "#a855f7", "#ec4899"];
  const color = colors[Math.abs(hashCode(user.id)) % colors.length];

  const session = liveblocks.prepareSession(user.id, {
    userInfo: {
      name: user.name,
      avatar: user.avatar,
      color,
    },
  });

  // Allow access to all rooms (restrict in production)
  const { body } = await request.json();
  session.allow(body.room, session.FULL_ACCESS);

  const { body: authBody, status } = await session.authorize();
  return new NextResponse(authBody, { status });
}

function hashCode(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = (hash << 5) - hash + str.charCodeAt(i);
    hash |= 0;
  }
  return hash;
}

Live Cursors

// components/LiveCursors.tsx
"use client";

import { useMyPresence, useOthers } from "@/liveblocks.config";
import { useCallback } from "react";

export function LiveCursors() {
  const [myPresence, updateMyPresence] = useMyPresence();
  const others = useOthers();

  const handlePointerMove = useCallback(
    (e: React.PointerEvent) => {
      updateMyPresence({
        cursor: { x: e.clientX, y: e.clientY },
      });
    },
    [updateMyPresence]
  );

  const handlePointerLeave = useCallback(() => {
    updateMyPresence({ cursor: null });
  }, [updateMyPresence]);

  return (
    <div
      className="fixed inset-0 pointer-events-auto"
      onPointerMove={handlePointerMove}
      onPointerLeave={handlePointerLeave}
    >
      {others.map(({ connectionId, presence, info }) => {
        if (!presence.cursor) return null;

        return (
          <div
            key={connectionId}
            className="absolute pointer-events-none transition-transform duration-75"
            style={{
              transform: `translate(${presence.cursor.x}px, ${presence.cursor.y}px)`,
            }}
          >
            <svg
              width="24"
              height="36"
              viewBox="0 0 24 36"
              fill="none"
              style={{ color: info?.color ?? "#000" }}
            >
              <path
                d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z"
                fill="currentColor"
              />
            </svg>
            <span
              className="absolute left-5 top-5 px-2 py-0.5 rounded text-xs text-white whitespace-nowrap"
              style={{ backgroundColor: info?.color ?? "#000" }}
            >
              {info?.name ?? presence.name ?? "Anonymous"}
            </span>
          </div>
        );
      })}
    </div>
  );
}

Collaborative Todo List

// components/CollaborativeTodos.tsx
"use client";

import { useStorage, useMutation, LiveObject } from "@/liveblocks.config";
import { useState } from "react";

export function CollaborativeTodos() {
  const todos = useStorage((root) => root.todos);
  const [newTodo, setNewTodo] = useState("");

  const addTodo = useMutation(({ storage }, text: string) => {
    const todos = storage.get("todos");
    todos.push(
      new LiveObject({
        id: crypto.randomUUID(),
        text,
        completed: false,
        createdBy: "current-user",
      })
    );
  }, []);

  const toggleTodo = useMutation(({ storage }, index: number) => {
    const todo = storage.get("todos").get(index);
    if (todo) {
      todo.set("completed", !todo.get("completed"));
    }
  }, []);

  const deleteTodo = useMutation(({ storage }, index: number) => {
    storage.get("todos").delete(index);
  }, []);

  if (!todos) return <div>Loading...</div>;

  return (
    <div className="max-w-md mx-auto">
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (newTodo.trim()) {
            addTodo(newTodo.trim());
            setNewTodo("");
          }
        }}
        className="flex gap-2 mb-4"
      >
        <input
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="Add a todo..."
          className="flex-1 border rounded px-3 py-2 text-sm"
        />
        <button
          type="submit"
          className="bg-primary text-primary-foreground px-4 py-2 rounded text-sm"
        >
          Add
        </button>
      </form>

      <ul className="space-y-2">
        {todos.map((todo, index) => (
          <li
            key={todo.id}
            className="flex items-center gap-3 p-3 border rounded"
          >
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(index)}
              className="accent-primary"
            />
            <span className={`flex-1 text-sm ${todo.completed ? "line-through text-muted-foreground" : ""}`}>
              {todo.text}
            </span>
            <button
              onClick={() => deleteTodo(index)}
              className="text-xs text-red-500"
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Room Provider Setup

// app/room/[id]/page.tsx
import { RoomProvider, LiveList } from "@/liveblocks.config";
import { ClientSideSuspense } from "@liveblocks/react";
import { CollaborativeTodos } from "@/components/CollaborativeTodos";
import { LiveCursors } from "@/components/LiveCursors";

export default async function RoomPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;

  return (
    <RoomProvider
      id={`room-${id}`}
      initialPresence={{ cursor: null, name: "User", color: "#3b82f6" }}
      initialStorage={{ todos: new LiveList([]), canvasObjects: new LiveMap() }}
    >
      <ClientSideSuspense
        fallback={<div className="flex justify-center py-12">Loading room...</div>}
      >
        <LiveCursors />
        <div className="container py-12">
          <h1 className="text-2xl font-bold mb-6">Collaborative Room</h1>
          <CollaborativeTodos />
        </div>
      </ClientSideSuspense>
    </RoomProvider>
  );
}

Need Real-Time Collaboration?

We build collaborative features including live editing, shared canvases, and real-time dashboards. Contact us to discuss your vision.

Liveblocksreal-timecollaborationCRDTReacttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles