Real-time collaboration for multiple users in React Flow projects with Yjs [E-BOOK]

Tomasz Świstak
May 8, 2025
2
min read

Get to know how to add multi-user and real-time collaboration to diagrams build in React Flow using Yjs.

Adding multi-user real-time collaboration to React Flow diagrams sounds like a great feature. It's complex, not so straightforward, but entirely possible. The real question is—how do we make it work? How to implement it into your software project seamlessly? Are there off-the-shelf solutions, or will we need to build from scratch? And what exactly do we need?

Multi-user real-time collaboration is a full-stack topic. It isn’t just about the front-end. For a smooth collaborative diagramming experience, we need to look at things from all angles—front-end (React, React Flow), back-end, and even the underlying architecture. In this post, we’ll explore the options, the challenges, and how to overcome them.

Multi-user real-time collaboration: What is it, and why does it matter?

When building rich client applications, sooner or later we must face the topic of concurrency. Users will need to share resources, view each other’s content, and sometimes even edit it—sounds like standard app behavior, right? However, it adds complexity. Allowing multi-user editing of the same document at the same time can lead to conflicts. While simple solutions like read-only modes or check-in/check-out mechanisms can help, they aren’t exactly the smooth, collaborative experiences users expect today. With apps like Google Docs or Miro setting the bar for real-time collaboration, multi-user editing is demanded wherever you look—with changes reflected live, seamlessly, and without friction.

This is where multi-user real-time collaboration comes in: it’s about techniques that allow multiple users to interact with the same document in real time, avoiding any possible conflicts. No matter if we’re dealing with a drawing canvas or a text document, the fundamental techniques for introducing multi-user editing are the same, but they’re different from the typical client-server model. Sounds confusing? I know. But don’t worry—we’ll break it all down for you.

A quick note: While this article focuses on React Flow, as collaborative diagramming and data visualization are our main areas of expertise at Synergy Codes, the insights we discuss here are broadly applicable to all types of multi-user real-time collaboration apps. That said, diagramming introduces unique challenges not typically found in text-based applications, and text editors come with their own set of issues. 

Available options for multi-user real-time collaboration

When it comes to adding multi-user real-time collaboration, you have the following options to consider:

  1. Writing your own custom solution
  2. Using libraries for multi-user real-time collaboration
  3. Leveraging services for live synchronization

Building a custom solution might be tempting for developers who love a good challenge, but I’d strongly advise against it. The complexities of multi-user real-time collaboration are often underestimated. Without a solid understanding of concurrent and distributed systems, you’re likely to run into issues that are already well-understood and easily solved by existing solutions.

So, we're left with two options for adding multi-user real-time collaboration into your software. 

Libraries are the most flexible solution since they can be integrated into almost any project. Most of them are implementations of CRDT (Conflict-free Replicated Data Type), which is a data structure designed specifically for distributed data systems and multi-user real-time collaboration. If you're really curious about CRDTs, there's a wealth of research on the topic, and the CRDT.tech website is a great place to start.

What libraries are available? Some of the more well-known ones include Yjs, Loro, Automerge, Logux, TinyBase. Keep in mind, though, that not all libraries will include communication protocols out of the box—they may just focus on conflict resolution through CRDTs. Before using them, dig into the documentation and read this article until the end.

On the other hand, services might come with more limitations, but using them is more straightforward. Some of them may even be easily integrated into your existing tech stack. Just to name a few: Liveblocks, Superviz, Electric, Supabase Realtime. Even if you decide to use a cloud service, it’s still worth reading through this article—since you may run into development and architectural issues that we describe here. Plus, some services actually rely on the same libraries mentioned earlier, so understanding the underlying technologies can still be helpful.

Yjs, a solution for multi-user real-time collaboration

At Synergy Codes, we strongly recommend Yjs for multi-user real-time collaboration. It’s a free, open-source, MIT-licensed JavaScript library implementing Yjs CRDT that can be used both on the client and server side.

One of the key advantages of Yjs is its flexibility—it doesn’t restrict you to specific use cases, giving you the freedom to adapt it to various projects. This flexibility ensures that Yjs can be used in everything from multi-user text editing to complex collaborative diagramming. Yjs is also lightweight, battle-tested, and ready for production use. Additionally, Yjs awareness allows real-time updates on who is currently interacting with your project. At Synergy Codes, we’ve successfully developed multi-user real-time collaboration apps with Yjs from scratch, as well as integrated it seamlessly into existing apps.

If you’re curious about our Yjs expertise or want to try it out yourself, check out our demo app.

While this article will now focus on using Yjs, the concepts and challenges we’ll discuss are present in other CRDT implementations, or even more broadly, in other multi-user real-time collaboration apps.

How to set up a React app with Yjs

Before diving deep into how Yjs works and how to integrate it with your app, let’s start with a hands-on example. We’ll build a simple React app using Yjs for state management to enable multi-user real-time collaboration. Once we get the basics down, we’ll transition to React Flow, but let’s first lay the foundation with something straightforward using multi-user real-time collaboration principles.

Initial app without multi-user real-time collaboration

I won’t walk you through the process of setting up a React project—you’re free to use whatever method you prefer (but I’d recommend steering clear of Create React App, since it’s deprecated). For this example, I’ve built a very simple to-do list app.

The key file here is use-todo.ts, as it contains the whole app's logic. By taking a look at the code, you’ll notice the state is made up of three variables:

  • todoItems – to-do items kept as a map: id -> TodoItem data.
  • order – map defining the order of items: id -> order.
  • currentEditedItem – points to the item that is being edited at the moment: keeps id or null. Used to display the editor for the proper entry.

Using a map for to-do items and defining order separately may seem unconventional for a typical to-do app, where items are often stored in arrays. However, this structure will make it much easier to adapt the code for Yjs, and it mirrors the kind of setup you might encounter in real use cases, such as collaborative diagramming apps.

The rest of the code isn’t crucial to this tutorial, as we’ll be focusing primarily on enabling multi-user real-time collaboration by modifying the app’s logic.

Adding Yjs to the app

Prerequisites

Before we integrate Yjs into our app, we need to install two packages:

  • yjs – The core Yjs package that provides collaborative state management.
  • y-websocket – A library that adds WebSocket connectivity for Yjs. It also comes with a simple Yjs WebSocket server that we’ll use.

Once the packages are installed, let's add a new entry to package.json for running a local Yjs WebSocket server with persisting data on a disk:

Now that we’ve added the Yjs WebSocket server, we can start it and let it run in the background with: npm run ws-server.

Yjs front-end setup

Now, let's revisit the use-todo.ts file. We need to start with two steps:

  • Initialize state management with Yjs.
  • Initialize Yjs WebSocket connection.

Unlike state management libraries like Redux, that keep everything in a store, Yjs keeps its data in a document, initialized using new Y.Doc().

Y.Doc itself can store only special data structures called shared data types. While they might remind you of Redux slices, they work differently. These data structures not only define how data is organized but also handle conflict resolution. There are six of them available and they cover all popular use cases:

  • Y.Map – Keeps data as a key-value storage.
    • If your app heavily relies on maps, you may want to check out YKeyValue which works the same but provides better optimization.
  • Y.Array – Keeps data as an array list.
  • Y.Text – Data structure optimized for text and rich text.
  • Y.XmlFragment, Y.XmlElement, Y.XmlText – Structures enabling multi-user real-time collaboration on XML-structured data.

Since our to-do app relies primarily on maps, we’ll use Y.Map. To create a new map in Y.Doc (or get an existing one), we use the doc.getMap() function. So, let's create the document with proper maps (we only need to synchronize todoItems and order, as currentEditedItem doesn't need to be synchronized):

However, creating Y.Doc itself only gives us state management. We still need connectivity between users. We’ll add it by instantiating the WebSocketProvider with the address of our Yjs WebSocket server:

The last line adds a simple logging mechanism, so we can track the connection status.

Notice that we define all of these outside of the React hook. That's because Yjs isn't a React solution and if not needed, we shouldn't keep it in React's lifecycle.

Synchronizing Yjs state to local state

Since Yjs isn't a React library, we need to write our own bindings. This involves handling two cases—reacting to changes in Yjs and sending changes to Yjs. Let's focus on the first one.

Each shared type implements an observer pattern by providing an observe function (along with unobserve). This allows us to define a callback that triggers whenever the data changes. To make things simple, we can tap into the existing useState. State can be easily accessed by converting the shared type with the toJSON() function. In the case of Y.Map, it will return a plain JavaScript object.

As it has to be done before rendering anything, we should create a useEffect without a dependency array for registering observers:

You might have noticed that we invoked functions despite setting them to observe. That's because they only trigger when Yjs gets new data, and we'd rather have an initial state. You also need to remember about unobserving in the effect's destructor to prevent memory leaks.

Synchronizing local changes to Yjs

At this point, our local state is in sync with what's happening in Yjs. However, when a user interacts with the to-do list, they're not just modifying Yjs—they're also updating the local state. Let's break down how we handle this with two examples.

Y.Map has a set(key, value) function. As expected, this function sets a value to a given key. Since our example relies solely on maps, that's the only thing that we’ll need to use.

First, let's check the addNew function, which adds a new to-do item to the state. Currently, it recreates objects in the state with an added new key-value pair:

In Yjs, there's no need to create a new map—we can simply set the new value. This way, the code gets simplified to:

Another scenario involves updating a specific value in an object. Since our to-do items are JavaScript objects, we can independently set the title and description. For example, updating the title currently looks like this:

In Yjs, we'll do it this way:

As you can see, we're recreating the object within the key-value pair. That's done so that Yjs is aware that there was a value change. It's aware of changes only on the level of shared types, but not deeper in them.

Following this same approach, you can update other functions as well, such as modifying the description or reordering items in the list.

Yjs awareness

In addition to shared types, Yjs providers use another mechanism for sharing changes: Yjs Awareness.

Yjs Awareness is used to share user state between all connected clients. By user state, we mean data that shouldn't be subject to conflict resolution, can only be edited by a single client at a time, and everyone should be aware of. A common example is cursor position, but Yjs Awareness can also be used in other situations.

Since Yjs Awareness is a part of the Yjs provider, we can find it in our WebsocketProvider, not in Y.Doc:

So, what can we do with it in our to-do app? For example, we have a local state that tracks what the user is editing. Why not share this information so all clients can see what each user is working on? This can be easily done in the following way:

As you can see, Yjs Awareness also implements the observer pattern, but the usage is a bit different than Y.Map. Instead of using observe(), we add an event listener with on(). In the observer, we iterate over each Yjs Awareness entry and copy them to our local state—excluding our own entry by checking the client ID.

Yjs Awareness itself is also a map, where client IDs are the keys, and the associated values can be anything. Now, it’s up to you to leverage this information in the UI. For example, you could highlight or change the color of the to-do item currently being edited by another user.

Summary of changes: Adding multi-user real-time collaboration

Adding multi-user real-time collaboration to our to-do app wasn't a hard task. We kept most of the original code intact and simply added some extra things to ensure integration with Yjs.

You can find the complete example here: https://github.com/synergycodes/yjs-example-react-todo-app

If you prefer, you can view only the diff of changes here: https://github.com/synergycodes/yjs-example-react-todo-app/compare/initial-app...main.

Here’s a quick rundown of what we did:

  • Added Yjs to the project.
  • Set up the Yjs WebSocket server and Yjs WebSocket provider.
  • Created maps in Yjs for keeping items and order: itemsMap and orderMap.
  • Added useEffect hooks with observers:
    • For itemsMap and orderMap copying their data to local state.
    • For Yjs Awareness to synchronize who's editing what.
  • Modified callbacks to use Yjs instead of local state: addNew, handleItemEdit (only Yjs Awareness), updateTitle, updateDescription, handleDragEnd (reordering).
  • To show Yjs Awareness data in the UI, we've added coloring of to-do items based on the user ID.

Remember, the to-do app is one of the simplest types of apps, so adding multi-user real-time collaboration to it wasn’t particularly challenging. As app complexity increases, so will the complexity of working with Yjs. However, these basic steps are the foundation you’ll work with most often when implementing multi-user real-time collaboration.

How to set up a React Flow app with Yjs

While to-do list apps are nice and simple, many of us don’t spend our professional lives working on them. Instead, we often dive into more complex projects. Let's explore how you can integrate Yjs with React Flow to build collaborative diagramming apps.

High-level overview before building a collaborative diagramming app

The basics remain the same: You’ll need Yjs, a connection provider, and Y.Doc. What differs is the content and usage. 

Here's a high-level overview of what needs to be done:

  • We need to instantiate Yjs connection provider and Y.Doc.
  • Inside Y.Doc we need to keep two Y.Map instances (or YKeyValue)—one for the nodes and the other for the edges.
  • We need to write a React code to synchronize the Yjs state with ReactFlow.
    • It's very similar to using ReactFlow with Zustand.
    • We need to have:
      • Reactive nodes and edges state variables (as arrays).
      • onNodesChange and onEdgesChange functions to synchronize ReactFlow changes with the state.

Additionally, you may want to synchronize cursor positions via Yjs Awareness, but for now, let's focus on getting the basics set up.

Synchronizing Yjs with React Flow when building a collaborative diagramming app

In React Flow, we operate on two important arrays in the state: nodes and edges, which together represent the entire graph. However, in Yjs, we won't keep it this way. Instead, a better structure for Yjs is a key-value map, as it allows for precise edits—a crucial factor for conflict resolution. Fortunately, our nodes and edges already have unique IDs, so there’s nothing stopping us from doing it this way.

Observing both structures is straightforward and works the same way as in our to-do list example. Things get interesting in updating Yjs, because React Flow requires us to implement onNodesChange and onEdgesChange functions. These functions send specific changes along with their type, which is great for targeted updates but also introduces some complexity.

For example, when writing onEdgesChange, we could write a code like this to handle different changes in Yjs:

The first case handles adding or replacing the edge, which is a straightforward use of the set() function. The second case deals with removing the edge.

Of course, this sample code isn’t a complete solution—there’s still more work to do. You’ll need to handle node changes, and it would be nice to have user cursors as well. 

Thankfully, with what we’ve covered so far, implementing these features should be straightforward. However, if you’re looking for a full working demo, React Flow Pro comes with an example of using React Flow with Yjs.

React Flow Pro example for collaborative diagramming

The React Flow Pro example is a great starting point for building multi-user real-time collaboration apps with React Flow and Yjs. While it’s a solid foundation for collaborative diagramming, there are areas where things could be optimized or handled differently.

Here’s how our approach differs:

  • We keep cursor states in Yjs Awareness instead of Y.Map.
  • We don't share fields like selected or dragging via Yjs.
    • The React Flow Pro example shares the whole state, but we prefer not to have all users share the same selection.
    • Instead, we use Yjs Awareness to indicate when another user has selected a node.
    • Some implementation details:
      • We keep non-synchronized values in a local state.
      • In onNodesChange and onEdgesChange, we separate which changes should be stored in Yjs and which should remain local.
      • When synchronizing Yjs state with React Flow, we merge local values.
      • Local state (or relevant parts of it) is sent separately via Yjs Awareness.

Consider these differences when moving the code into your own app. It's also worth noting that Yjs includes built-in undo/redo functionality, which is not a part of this implementation—but could be a valuable addition.

The architectural challenges of multi-user real-time collaboration apps

From the example we just explored, Yjs might seem like just another state management library. Aside from running an additional server, there were no major changes to the technical stack. That’s the beauty of Yjs: it’s that simple! But bear in mind that this was a very basic use case—in real-world applications, things get more complex.

When working with Yjs—or multi-user real-time collaboration apps, in general—we typically face three implementation scenarios:

  • New projects – When starting from scratch, we can design the entire software project properly with multi-user real-time collaboration in mind. This gives us full control, but it also requires a deep understanding of Yjs’ capabilities and limitations.
  • Adding a multi-user editor to an existing app – Adding a new part to an existing software project means working within an established architecture. This introduces integration challenges, requiring careful planning to ensure compatibility of the multi-user editor with the rest of the system.
  • Adding multi-user real-time collaboration to an existing editor – The most complex scenario is adding multi-user real-time collaboration to an existing editor. Here, we must ensure that Yjs doesn’t interfere with established functionality. Simply swapping out existing state management or saving function for Yjs’ synchronization won’t work in most cases.

Yjs operates differently from typical client-server or request-response solutions. To use Yjs effectively, we need to understand it on multiple levels. Let me guide you through the most common problems we've encountered and how to tackle them.

Read the full version

Fill the form below to get an e-book where you'll find the full version of this guide with additional information on:

  • Communication model of Yjs.
  • Scaling Yjs WebSocket servers.
  • Organizing and persisting data in Yjs.
  • Thinking in Yjs vs. client-server thinking.
  • Conflict resolution in Yjs CRDT.
  • Yjs integration into an existing system.
  • Multi-document approach.
  • Using Yjs with front-end frameworks.
Contact details
By sending a message you allow Synergia Pro Sp. z o.o., with its registered office in Poland, Wroclaw (51-607) Czackiego Street 71, to process your personal data provided by you in the contact form for the purpose of contacting you and providing you with the information you requested. You can withdraw your consent at any time. For more information on data processing and the data controller please refer to our Privacy policy.
*Required
Thank you! Your submission has been received!
Click button below to download e-book "Real-time collaboration for multiple users in React Flow projects with Yjs"
Oops! Something went wrong while submitting the form.
Tomasz Świstak
JavaScript Developer at Synergy Codes

He’s primarily working with React and NestJS but is open to new frameworks and libraries, whatever suits the project the best. Tomasz is also keen on algorithmic and researching optimal ways to solve complex problems, despite his expertise in Web applications. Implementing complex features and applying scientific theory practically is his main area of interest. 

Get more from me on:
Share:

Articles you might be interested in

React Flow: Everything you need to know

In this guide, you'll learn all essential concepts about React Flow. You're new to the topic? This React Flow tutorial will get you started!

Content team
Apr 25, 2025

Workflow application: what is it and how to use it?

A guide to understanding workflow management applications, their features, benefits, challenges, and steps for effective implementation.

Content team
Feb 27, 2025

The ultimate guide to optimize React Flow project performance [E-BOOK]

Follow these step-by-step process to enhance your React Flow project's performance.

Łukasz Jaźwa
Jan 23, 2025