Tutoriel Liveblocks 2.0 en 2026 : Collaboration Temps Réel avec Next.js 15

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

La collaboration temps réel était autrefois un projet de six mois : monter un cluster WebSocket, choisir un CRDT, écrire un protocole de présence, concevoir une stratégie de résolution de conflits et prier pour que le client se reconnecte proprement quand un utilisateur ferme son ordinateur. Liveblocks 2.0 condense tout cela dans un service géré avec des hooks React de premier ordre. Vous insérez le provider dans votre arbre Next.js, vous appelez useOthers() ou useStorage(), et votre application gagne le multijoueur façon Figma gratuitement.

Dans ce tutoriel, vous allez construire une application de canvas collaboratif depuis zéro avec Liveblocks 2.0 et Next.js 15. À la fin, plusieurs utilisateurs verront les curseurs des autres, partageront leur présence, laisseront des commentaires en fil sur le canvas et modifieront une liste partagée de notes adhésives qui restera cohérente même si quelqu'un perd la connexion en plein milieu d'une édition.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20 ou plus récent installé
  • Un compte Liveblocks gratuit (inscription sur liveblocks.io)
  • De l'aisance avec React, TypeScript et l'App Router de Next.js
  • Une compréhension de base des schémas d'authentification
  • Un éditeur de code (VS Code recommandé)

Ce Que Vous Allez Construire

À la fin de ce tutoriel, vous aurez :

  1. Une application Next.js 15 avec le provider Liveblocks branché à l'App Router
  2. Des curseurs en direct qui suivent les autres utilisateurs en temps réel
  3. Des avatars de présence montrant qui est actuellement dans la salle
  4. Un canvas partagé de notes adhésives synchronisé via Liveblocks Storage
  5. Des fils de commentaires ancrés aux éléments du canvas
  6. Une authentification par jeton pour que seuls les utilisateurs connectés rejoignent les salles
  7. Un déploiement prêt pour la production sur Vercel

Étape 1 : Configuration du Projet

Créez une nouvelle application Next.js 15 avec TypeScript et Tailwind. Utilisez l'App Router : les hooks de Liveblocks 2.0 et les nouvelles primitives de commentaires attendent les React Server Components.

npx create-next-app@latest liveblocks-canvas \
  --typescript --tailwind --app --eslint \
  --src-dir --import-alias "@/*"
cd liveblocks-canvas

Installez les paquets Liveblocks dont vous aurez besoin. La séparation est intentionnelle : @liveblocks/client est le cœur indépendant du framework, @liveblocks/react expose les hooks et @liveblocks/react-ui fournit des composants prêts à l'emploi pour les commentaires et les notifications.

npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui
npm install -D @liveblocks/node

@liveblocks/node alimente le point d'authentification côté serveur que nous construirons à l'étape 4.

Étape 2 : Configurer Votre Projet Liveblocks

Ouvrez le tableau de bord Liveblocks et créez un nouveau projet. Notez trois valeurs depuis la page des clés API :

  1. Clé publique — utilisable côté navigateur ; pratique pour le prototypage
  2. Clé secrète — uniquement côté serveur ; sert à émettre des jetons d'accès aux salles
  3. ID de projet — visible dans l'URL du projet

Ajoutez-les à .env.local :

LIVEBLOCKS_SECRET_KEY=sk_dev_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=pk_dev_xxxxxxxxxxxxxxxxxxxxx

Ne committez jamais la clé secrète. La clé publique convient pour le développement, mais les applications de production doivent toujours utiliser la clé secrète avec un point d'authentification par jeton, que nous allons construire bientôt.

Étape 3 : Définir Vos Types Liveblocks

Liveblocks 2.0 s'appuie fortement sur TypeScript. Vous déclarez la forme de la présence, du stockage, des métadonnées utilisateur et des métadonnées de fil dans un type global, et tous les hooks en aval sont entièrement typés. Créez src/liveblocks.config.ts :

import { LiveList, LiveObject } from "@liveblocks/client";
 
export type StickyNote = LiveObject<{
  id: string;
  x: number;
  y: number;
  text: string;
  color: "yellow" | "pink" | "blue" | "green";
  authorId: string;
}>;
 
declare global {
  interface Liveblocks {
    Presence: {
      cursor: { x: number; y: number } | null;
      selectedNoteId: string | null;
    };
    Storage: {
      notes: LiveList<StickyNote>;
    };
    UserMeta: {
      id: string;
      info: {
        name: string;
        avatar: string;
        color: string;
      };
    };
    ThreadMetadata: {
      noteId: string;
    };
  }
}

Deux points à souligner :

  • Presence est un état éphémère diffusé aux autres utilisateurs de la salle (position du curseur, sélection courante). Il disparaît à la seconde où quelqu'un se déconnecte.
  • Storage est un état partagé persistant stocké sur les serveurs Liveblocks. LiveList et LiveObject sont des structures de données sans conflit qui fusionnent automatiquement les éditions concurrentes.

Étape 4 : Construire le Point d'Authentification

Les applications de production ne doivent jamais exposer la clé publique directement. Créez plutôt une route serveur qui authentifie l'utilisateur via votre session existante, puis demande à Liveblocks un jeton d'accès lié à la salle. Créez src/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) {
  const user = await getCurrentUser(request);
  if (!user) {
    return new NextResponse("Unauthorized", { status: 401 });
  }
 
  const { room } = await request.json();
 
  const session = liveblocks.prepareSession(user.id, {
    userInfo: {
      name: user.name,
      avatar: user.avatar,
      color: pickColor(user.id),
    },
  });
 
  session.allow(room, session.FULL_ACCESS);
 
  const { status, body } = await session.authorize();
  return new NextResponse(body, { status });
}
 
async function getCurrentUser(request: NextRequest) {
  return {
    id: "user-" + Math.random().toString(36).slice(2, 10),
    name: "Demo User",
    avatar: "/avatars/default.png",
  };
}
 
function pickColor(userId: string) {
  const colors = ["#6366f1", "#ec4899", "#10b981", "#f59e0b", "#06b6d4"];
  const index = userId.charCodeAt(userId.length - 1) % colors.length;
  return colors[index];
}

Remplacez getCurrentUser par votre vraie logique de session : Auth.js, Clerk, Better Auth ou ce que vous utilisez déjà. L'appel à session.allow() accorde l'accès à une salle spécifique avec FULL_ACCESS. Utilisez READ_ACCESS pour les lecteurs invités.

Étape 5 : Monter le Provider de Salle

Créez src/components/Room.tsx pour envelopper les pages qui ont besoin de fonctionnalités multijoueurs. Le provider gère le cycle de vie WebSocket, la logique de reconnexion et la diffusion de présence pour vous.

"use client";
 
import { ReactNode } from "react";
import {
  LiveblocksProvider,
  RoomProvider,
  ClientSideSuspense,
} from "@liveblocks/react/suspense";
import { LiveList } from "@liveblocks/client";
 
export function Room({
  children,
  roomId,
}: {
  children: ReactNode;
  roomId: string;
}) {
  return (
    <LiveblocksProvider authEndpoint="/api/liveblocks-auth">
      <RoomProvider
        id={roomId}
        initialPresence={{ cursor: null, selectedNoteId: null }}
        initialStorage={{ notes: new LiveList([]) }}
      >
        <ClientSideSuspense fallback={<CanvasSkeleton />}>
          {children}
        </ClientSideSuspense>
      </RoomProvider>
    </LiveblocksProvider>
  );
}
 
function CanvasSkeleton() {
  return (
    <div className="flex h-screen items-center justify-center text-zinc-500">
      Connexion à la salle...
    </div>
  );
}

Quelques détails importants :

  • ClientSideSuspense est la manière recommandée de bloquer le contenu qui dépend du Storage. Il attend l'instantané initial avant le rendu.
  • initialPresence et initialStorage ne s'exécutent que pour le tout premier utilisateur d'une salle. Tous les autres reçoivent l'état existant.
  • Le chemin d'import suspense active l'intégration React Suspense ; la version sans suspense renvoie des valeurs optionnelles à vérifier pour null.

Étape 6 : Afficher les Curseurs en Direct

Les curseurs en direct sont l'effet emblématique de Figma. Ils sont d'une simplicité enfantine dans Liveblocks 2.0, car les données de curseur vivent dans Presence — diffusées à 60 images par seconde, nettoyées automatiquement à la déconnexion.

Créez src/components/LiveCursors.tsx :

"use client";
 
import { useMyPresence, useOthers } from "@liveblocks/react/suspense";
import { useEffect } from "react";
 
export function LiveCursors() {
  const [, updateMyPresence] = useMyPresence();
  const others = useOthers();
 
  useEffect(() => {
    function onPointerMove(event: PointerEvent) {
      updateMyPresence({
        cursor: { x: event.clientX, y: event.clientY },
      });
    }
    function onPointerLeave() {
      updateMyPresence({ cursor: null });
    }
 
    window.addEventListener("pointermove", onPointerMove);
    window.addEventListener("pointerleave", onPointerLeave);
    return () => {
      window.removeEventListener("pointermove", onPointerMove);
      window.removeEventListener("pointerleave", onPointerLeave);
    };
  }, [updateMyPresence]);
 
  return (
    <>
      {others.map(({ connectionId, presence, info }) => {
        if (!presence.cursor) return null;
        return (
          <Cursor
            key={connectionId}
            x={presence.cursor.x}
            y={presence.cursor.y}
            color={info?.color ?? "#6366f1"}
            name={info?.name ?? "Anonymous"}
          />
        );
      })}
    </>
  );
}
 
function Cursor({
  x,
  y,
  color,
  name,
}: {
  x: number;
  y: number;
  color: string;
  name: string;
}) {
  return (
    <div
      className="pointer-events-none fixed left-0 top-0 z-50 transition-transform duration-75"
      style={{ transform: `translate(${x}px, ${y}px)` }}
    >
      <svg width="20" height="20" viewBox="0 0 20 20" fill={color}>
        <path d="M3 3l14 5-6 2-2 6z" />
      </svg>
      <span
        className="ml-3 rounded-md px-2 py-0.5 text-xs font-medium text-white shadow-md"
        style={{ background: color }}
      >
        {name}
      </span>
    </div>
  );
}

useOthers() renvoie un tableau de tous les autres utilisateurs connectés avec leur présence et métadonnées. Il ne fait un nouveau rendu que lorsque le sous-ensemble pertinent change, donc ajouter des curseurs est essentiellement gratuit même avec des dizaines de collaborateurs.

Étape 7 : Ajouter les Avatars de Présence

Une pile d'avatars flottants indique aux utilisateurs qui d'autre est dans la salle sans les forcer à courir après les curseurs.

"use client";
 
import { useOthers, useSelf } from "@liveblocks/react/suspense";
 
export function ActiveCollaborators() {
  const others = useOthers();
  const self = useSelf();
 
  return (
    <div className="fixed right-4 top-4 flex items-center gap-2">
      <span className="text-sm text-zinc-500">
        {others.length + 1} en ligne
      </span>
      <div className="flex -space-x-2">
        {self ? (
          <Avatar
            name={self.info?.name ?? "Vous"}
            color={self.info?.color ?? "#6366f1"}
            isSelf
          />
        ) : null}
        {others.slice(0, 4).map(({ connectionId, info }) => (
          <Avatar
            key={connectionId}
            name={info?.name ?? "Anon"}
            color={info?.color ?? "#10b981"}
          />
        ))}
        {others.length > 4 ? (
          <span className="ml-2 text-xs text-zinc-500">
            +{others.length - 4}
          </span>
        ) : null}
      </div>
    </div>
  );
}
 
function Avatar({
  name,
  color,
  isSelf,
}: {
  name: string;
  color: string;
  isSelf?: boolean;
}) {
  return (
    <div
      className="flex h-9 w-9 items-center justify-center rounded-full border-2 border-white text-sm font-semibold text-white shadow"
      style={{ background: color, outline: isSelf ? "2px solid #111" : "none" }}
      title={name}
    >
      {name.charAt(0).toUpperCase()}
    </div>
  );
}

useSelf() renvoie l'utilisateur courant avec ses métadonnées émises par jeton. Combinez-le avec useOthers() pour afficher la liste complète des participants.

Étape 8 : Synchroniser le Canvas de Notes Adhésives

Maintenant la partie consistante : l'état partagé. Nous allons stocker un tableau de notes adhésives dans Liveblocks Storage afin que chaque utilisateur voie le même canvas, et la résolution de conflits se fera gratuitement.

"use client";
 
import {
  useMutation,
  useStorage,
  useSelf,
} from "@liveblocks/react/suspense";
import { LiveObject } from "@liveblocks/client";
import { nanoid } from "nanoid";
 
export function StickyCanvas() {
  const notes = useStorage((root) => root.notes);
  const self = useSelf();
 
  const addNote = useMutation(({ storage }, x: number, y: number) => {
    const note = new LiveObject({
      id: nanoid(),
      x,
      y,
      text: "Nouvelle note",
      color: "yellow" as const,
      authorId: self?.id ?? "anonymous",
    });
    storage.get("notes").push(note);
  }, [self?.id]);
 
  const updateNote = useMutation(
    ({ storage }, id: string, patch: Partial<{ text: string; x: number; y: number }>) => {
      const list = storage.get("notes");
      for (let index = 0; index < list.length; index++) {
        const note = list.get(index);
        if (note?.get("id") === id) {
          note.update(patch);
          return;
        }
      }
    },
    [],
  );
 
  function onCanvasDoubleClick(event: React.MouseEvent) {
    addNote(event.clientX, event.clientY);
  }
 
  return (
    <div
      onDoubleClick={onCanvasDoubleClick}
      className="relative h-screen w-full bg-zinc-50"
    >
      {notes.map((note) => (
        <StickyNote
          key={note.id}
          note={note}
          onChange={(text) => updateNote(note.id, { text })}
        />
      ))}
      <p className="absolute bottom-4 left-4 text-sm text-zinc-400">
        Double-cliquez n'importe où pour ajouter une note
      </p>
    </div>
  );
}

Trois choses à remarquer :

  • useStorage accepte un sélecteur — il ne fait un nouveau rendu que lorsque la portion sélectionnée change, donc modifier une note ne re-rend pas les autres.
  • useMutation est la seule manière sûre d'écrire dans Storage. Liveblocks enregistre la mutation, la diffuse aux pairs et fusionne les écritures concurrentes de manière déterministe.
  • LiveObject et LiveList utilisent un algorithme de type Yjs sous le capot, donc les éditions concurrentes ne s'écrasent jamais silencieusement.

Un éditeur StickyNote minimal :

function StickyNote({
  note,
  onChange,
}: {
  note: { id: string; x: number; y: number; text: string; color: string };
  onChange: (text: string) => void;
}) {
  return (
    <div
      className="absolute w-48 rounded-md p-3 shadow-lg"
      style={{
        left: note.x,
        top: note.y,
        background: "#fef08a",
      }}
    >
      <textarea
        defaultValue={note.text}
        onBlur={(event) => onChange(event.target.value)}
        className="h-24 w-full resize-none bg-transparent text-sm outline-none"
      />
    </div>
  );
}

Étape 9 : Ajouter des Commentaires en Fil

Liveblocks 2.0 livre une primitive de commentaires qui gère le filage, les réactions et les accusés de lecture prêts à l'emploi. Ancrez un fil à chaque note adhésive en définissant noteId dans ThreadMetadata.

"use client";
 
import {
  Composer,
  Thread,
} from "@liveblocks/react-ui";
import { useThreads } from "@liveblocks/react/suspense";
 
export function NoteComments({ noteId }: { noteId: string }) {
  const { threads } = useThreads({
    query: { metadata: { noteId } },
  });
 
  return (
    <div className="space-y-4">
      {threads.map((thread) => (
        <Thread key={thread.id} thread={thread} />
      ))}
      <Composer
        metadata={{ noteId }}
        placeholder="Ajouter un commentaire..."
      />
    </div>
  );
}

Importez les styles par défaut une fois dans votre layout racine :

import "@liveblocks/react-ui/styles.css";

Les composants Thread et Composer rendent une interface entièrement accessible et compatible mode sombre. Ils gèrent les mises à jour optimistes, l'auto-complétion des mentions et le rendu Markdown pour vous.

Étape 10 : Tout Câbler Ensemble

Enfin, la page. Passez un identifiant de salle unique par tableau pour que les différents documents restent isolés.

import { Room } from "@/components/Room";
import { LiveCursors } from "@/components/LiveCursors";
import { ActiveCollaborators } from "@/components/ActiveCollaborators";
import { StickyCanvas } from "@/components/StickyCanvas";
 
export default function BoardPage({
  params,
}: {
  params: { boardId: string };
}) {
  return (
    <Room roomId={`board-${params.boardId}`}>
      <ActiveCollaborators />
      <StickyCanvas />
      <LiveCursors />
    </Room>
  );
}

Lancez le serveur de développement, ouvrez la même URL de tableau dans deux fenêtres de navigateur et regardez vos curseurs se suivre en temps réel.

npm run dev

Étape 11 : Optimiser pour la Production

Trois leviers à activer avant le déploiement :

Throttlez les mises à jour de présence. Liveblocks throttle déjà les diffusions de curseur sur le réseau, mais vous pouvez réduire le travail côté client en passant throttle: 16 au LiveblocksProvider pour des curseurs à 60 fps, ou throttle: 50 pour 20 fps si la précision n'est pas critique.

Utilisez Status pour la conscience de connexion. useStatus() renvoie l'état de la connexion ("connecting", "connected", "reconnecting", "disconnected"). Affichez une bannière quand il passe à "reconnecting" pour que les utilisateurs sachent qu'il faut attendre avant les grosses éditions.

Définissez un timeout d'inactivité de salle. Dans le tableau de bord Liveblocks, configurez votre projet pour déconnecter automatiquement les salles après une période d'inactivité. Cela maintient votre nombre mensuel de connexions actives prévisible.

Étape 12 : Déployer sur Vercel

Liveblocks fonctionne parfaitement avec Vercel. Poussez votre code sur GitHub, importez le dépôt dans Vercel et ajoutez deux variables d'environnement :

  • LIVEBLOCKS_SECRET_KEY (clé de production depuis le tableau de bord Liveblocks)
  • NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY (uniquement si vous utilisez encore le repli sur la clé publique)

C'est tout. Liveblocks tourne sur sa propre infrastructure distribuée mondialement, donc vous n'avez pas besoin d'approvisionner Redis, de gérer un cluster WebSocket ou de vous soucier des démarrages à froid sur le serverless.

Tester Votre Implémentation

Ouvrez l'URL déployée dans deux navigateurs différents (ou une fenêtre normale et une fenêtre privée). Vous devriez voir :

  • Les deux curseurs se suivre en douceur en temps réel
  • Les deux avatars apparaître dans la pile en haut à droite
  • Les notes adhésives apparaître instantanément sur les deux écrans quand un utilisateur double-clique
  • Les fils de commentaires attachés aux notes se synchroniser entre les fenêtres
  • Une reconnexion gracieuse quand vous activez le mode avion brièvement

Dépannage

"Cannot read property of undefined" dans une frontière Suspense. Vous avez oublié d'envelopper ce composant avec ClientSideSuspense ou vous avez importé depuis @liveblocks/react au lieu de @liveblocks/react/suspense.

Les curseurs n'apparaissent pas. Vérifiez que le composant curseur est rendu au-dessus du canvas en z-index, et que useMyPresence().cursor est bien mis à jour. Un bug courant est d'oublier de définir cursor à null sur pointerleave, ce qui laisse des curseurs périmés bloqués.

Les mutations Storage échouent silencieusement. Les mutations ne s'exécutent qu'à l'intérieur de useMutation. Appeler storage.get(...).push(...) en dehors de ce hook ne fait rien, car Liveblocks a besoin du contexte transactionnel pour diffuser le changement.

Bloqué sur "Connexion à la salle..." Votre point d'authentification renvoie une mauvaise forme. Assurez-vous que le corps de la réponse est le corps brut renvoyé par session.authorize() et que le code d'état est propagé.

Étapes Suivantes

  • Ajoutez l'intégration Yjs pour alimenter un éditeur de texte riche collaboratif via @liveblocks/yjs
  • Persistez les notes adhésives dans votre propre base de données via l'API REST Liveblocks Storage et les webhooks
  • Ajoutez les notifications avec @liveblocks/react-ui pour que les utilisateurs soient notifiés des mentions
  • Combinez avec TanStack Query v5 pour un état hybride client-serveur
  • Couplez avec Better Auth pour une authentification de qualité production

Conclusion

Liveblocks 2.0 transforme le multijoueur en une fonctionnalité que vous livrez en un après-midi, pas en un trimestre. En vous appuyant sur Presence pour l'état éphémère, Storage pour l'état persistant sans conflit et les primitives de commentaires pour l'expérience de collaboration, vous obtenez l'expérience Figma sans exploiter aucune des pièces difficiles. Commencez par une seule surface collaborative dans votre application — généralement un tableau de bord, un canvas de planification ou un éditeur de document — et laissez vos utilisateurs vous dire quoi rendre multijoueur ensuite.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Créer des emails transactionnels avec Resend et React Email dans Next.js.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire une Application Full-Stack avec Appwrite Cloud et Next.js 15

Apprenez à construire une application full-stack complète en utilisant Appwrite Cloud comme backend-as-a-service et Next.js 15 App Router. Ce tutoriel couvre l'authentification, les bases de données, le stockage de fichiers et les fonctionnalités temps réel.

30 min read·