Skip to Content
🚨 New Example: Handling Node Collisions!

Multiplayer

Node-based UIs are often used to create applications that are visual and explorative in their nature. For collaborative features, this usually means that there is no satisfying “middle-ground” solution like sparsely synchronising the state between users, because it would break visual consistency or make collaborative exploration more complicated. Often, a full live multiplayer system needs to be put in place.

In our Collaborative Flow Pro Example, we show a realtime multiplayer collaboration flow using React Flow and Yjs . In this guide, we will explore some best practices for integrating multiplayer collaboration in your React Flow application.

What are multiplayer applications?

Although realtime collaboration would be the correct term here, we refer to it as multiplayer, which was most likely popularized in this context by Figma. Besides being more intuitive for marketing purposes, it also does a better job of communicating the commitment to creating something truly realtime by putting it in the same category as competitive video games.

To figure out what degree of collaboration your application supports, you just need to look at how closely it resembles a multiplayer game. These features likely include:

  • No manual saving is required; all users share a world where actions are persisted.
  • Any changes made by other users are immediately visible to you.
  • Not only completed actions, but transitive states are synced too (e.g. dragging a node, not only when the node is dropped).
  • You can see other users (e.g. their cursors or viewport positions)

A word of caution

Making multiplayer applications is hard! When users are collaboratively editing shared objects, conflicts, delays, connection drops and concurrent operations are expected. The hardest challenge in multiplayer apps is conflict-resolution: you always need some kind of sync engine that is able to merge any changes coming from the server (or other clients) into the (often optimistic) client state. You need to gracefully handle conflicts and disconnects. You need to build a reliable network layer.

The simplest sync engine for multiplayer would be a “first-come-first-served” approach, and failed requests and messages sent being ignored. If you try to manually handle this, you will see a quite vast variety of edge cases emerge. Networks are slow, clients disconnect, and the order of actions matters.

Local-first and CRDTs

Depending on how far you want to take handling conflicts and disconnects, you very quickly land in local-first territory. When your sync engine allows a user to be disconnected for an indefinite amount of time, you have a local-first application. If you would like a good primer on this topic we recommend Ink & Switch’s essay  that coined the term.

A CRDT (Conflict-free Replicated Data Type) is a data structure that lets multiple users edit the same data simultaneously on different devices, then automatically merges all changes without conflicts—no central server needed. Instead of just storing the current value of the data, CRDTs keep operation history on every connected client, so any replica can merge changes into the same final state, regardless of the order updates arrive. They are thus solving the conflicts and disconnects by being multi-replica synced data structures that can work completely offline, and automatically upload, fetch, and reconcile their state on reconnection.

A couple of popular solutions are Yjs , Automerge  and Loro .

What React Flow state should I sync?

Among your first considerations should be how you want to sync what parts of the local state of your app.

Ephemeral vs Durable

While there are many ways to sync state between clients, we can categorize them into two groups: ephemeral and durable (aka atomic) changes. Ephemeral changes are not persisted and neither its consistency nor its correctness is very important for the functionality of the application. These are things like cursor or viewport positions or any transient interaction state. Missing some of these updates will not break anything and in case of a connection loss, a client can just listen for the newest changes and restore any lost functionality.

On the other hand, updates to the nodes & edges should be persistent and consistent! Imagine if Alice deletes a node and Bob misses this update. Now if Bob subsequently moves the node, we would have an inconsistent application state. For this kind of data we have to use a solution that handles disconnects more aggressively and is able to discard impossible actions like moving a deleted node.

At the core of a multiplayer React Flow application is syncing the nodes and edges. However not all parts of the state should be synced. For instance, what nodes and edges are selected should be different for each client. Also there are some fields that are relevant for the certain library functions like dragging, resizing or measured.

This is an overview of what parts are recommended to sync and what not.

Nodes

FieldDurableEphemeralExplanation
id✅❌Important, always needs to be in-sync
type✅❌Important, always needs to be in-sync
data✅❌Important, always needs to be in-sync
position✅✳️Important, includes transient state
width, height✅✳️Important, includes transient state
dragging❌✅Transient interaction state
resizing❌✅Transient interaction state
selected❌❌Per-user UI state
measured❌❌Computed from DOM

Edges

FieldDurableEphemeralExplanation
id✅❌Important, always needs to be in-sync
type✅❌Important, always needs to be in-sync
data✅❌Important, always needs to be in-sync
source✅❌Important, always needs to be in-sync
target✅❌Important, always needs to be in-sync
sourceHandle✅❌Important, always needs to be in-sync
targetHandle✅❌Important, always needs to be in-sync
selected❌❌Per-user UI state

Connections and Cursors

Good examples of ephemeral data are connections (the transient part of edges being created) and cursors.

Sharing each user’s connections status improves visual consistency across clients, but losing this state on disconnect doesn’t affect the correctness of the shared data: it’s purely presentational.

The same applies to cursors. They help create the immersion of a shared workspace, but they’re not essential to the application’s integrity. As long as the nodes and edges remain in sync, cursor state can be safely discarded.

Both can be shared as purely ephemeral state. If you want to reduce the frequency of live updates, you can debounce the updates and smooth the movements of other users’ cursors. You can use a library like perfect-cursors  to smooth the movements of other users’ cursors.

Third Party Libraries and Services

We experimented with different multiplayer backend solutions to understand their best use cases for React Flow apps. What we found out, is that there’s no one-size-fits-all solution. The choice between CRDT-based local-first libraries (like Yjs  and Jazz ) and server-authoritative approaches (like Supabase  and Convex ) will change the way you approach building multiplayer React Flow applications:

  • CRDT libraries will make your life easier around offline-first multiplayer support, giving you for-free automatic conflict resolution, but in the case where your application is also structured around a database, you will need to implement your own adapters between the CRDT library and the database.
  • Server-authoritative solutions are easier to use with a database and classical application logic, but will complicate your life around offline-first multiplayer support, conflict resolution, and development complexity.
NameArchitectureOffline SupportConflict ResolutionNotes
YjsCRDT (Local-first)âś… Full offline supportAutomatic (CRDT)Collaborative editing, offline-first CRDT for local-first apps, with a React Flow Pro Example
JazzCRDT (Local-first)âś… Full offline supportAutomatic (CRDT)A full, offline-first database solution that handles multiplayer natively.
SupabaseServer-authoritative❌ Requires connectionManual/RLS policiesClassic SQL-based database solution, with realtime support, but no CRDT (conflict resolution must be implemented manually).
ConvexServer-authoritative❌ Requires connectionOptimistic TypeScript-first, database solution with real-time queries and a sync-engine with some conflict resolution capabilities. Can be paired with Automerge 
LiveblocksServer-authoritative, CRDT-like🟡 Partial offline supportUses CRDTs under the hoodReal-time collaboration platform with hosted proprietary backend. React Flow Example .
VeltCRDT with managed backend✅ Full offline supportUses CRDTs under the hoodCollaborative editing platform with managed backend, with a dedicated React Flow Library 
AutomergeCRDT (Local-first)✅ Full offline supportAutomatic (CRDT)The canonical CRDT library. Can be used together with Convex  and other databases.
LoroCRDT (Local-first)âś… Full offline supportAutomatic (CRDT)A fast CRDT library in Rust and WebAssembly with history tracking and version control.
Last updated on