The other day, someone I know messaged:
"I found myself over the holiday trying to explain to my son how investment returns worked over Zoom and it would’ve been so easy if we had both been able to work inside of something like a Google Doc"
Challenge accepted!
During the past few days I had fun building an investment return simulator, where we can forecast investment returns based on multiple parameters.
The internet is full of such websites, but in this one, I wanted to explore how we can embed collaborative features, and how they can bring people together.
In this guide, we'll see how we can add real-time collaboration to a regular React app in just 10 minutes without writing any backend code or dealing with web sockets. As a bonus, we'll also explore how we can enhance user experience in collaborative websites!
The full source code for this project is available on GitHub
Let's dive in!
Before diving into the collaborative features, we needed a solid foundation. I began by creating a single-player version where users could enter their investment parameters. These inputs would then feed into a calculation engine that generates and displays investment return forecasts.
I used bolt.new to get up and running quickly. I was impressed by the clean design it gave me, and with how fast I got to an acceptable starting point. Despite the huge head start, I still needed to fine tune quite some calculation logic, and adjust the UI to my liking.
With the single-player version complete, I turned my attention to making it collaborative. This project presented a perfect opportunity to test React Together, an open-source library I've been developing at Multisynq for the past few months.
React Together provides hooks and components that enable collaborative features without the complexity of setting up backends or managing socket connections. The implementation process proved straightforward, though it revealed some areas for improvement that we'll address in future versions of the library.
Now, let's walk through the three steps to add real-time collaboration to our app! Start your timers ?
The first step is wrapping our application in a React Together context provider. This component handles all the state synchronization and session management behind the scenes.
// src/index.tsx import { StrictMode } from 'react' import { ReactTogether } from 'react-together' import { createRoot } from 'react-dom/client' import App from './App.tsx' import './index.css' createRoot(document.getElementById('root')!).render( <StrictMode> <ReactTogether sessionParams={{ appId: import.meta.env['VITE_APP_ID'], apiKey: import.meta.env['VITE_API_KEY'], }}> <App /> </ReactTogether> </StrictMode>, )
React Together uses Multisynq's infrastructure for application synchronization, which requires an API key. You can get your free API key from multisynq.io/account. And don't worry, these keys are meant to be public, since you can control which domains can use them.
We could configure React Together to automatically connect all users to the same session once they enter the website. In fact, that would make this a 2-step guide, but I went for a Google Docs-style approach where collaboration is opt-in. Users remain disconnected until they explicitly create or join a session through a button click. We will cover session management on the third step of this guide!
With React Together set up, the next step is to synchronize state between users. This process is incredibly simple: we just need to replace React's useState hooks with React Together's useStateTogether hooks.
The useStateTogether hook works similarly to useState, but requires an additional rtKey parameter. This key uniquely identifies the state across the application, ensuring proper synchronization even in responsive layouts where DOM hierarchies might differ between viewports.
Here's how the transformation looks:
// src/index.tsx import { StrictMode } from 'react' import { ReactTogether } from 'react-together' import { createRoot } from 'react-dom/client' import App from './App.tsx' import './index.css' createRoot(document.getElementById('root')!).render( <StrictMode> <ReactTogether sessionParams={{ appId: import.meta.env['VITE_APP_ID'], apiKey: import.meta.env['VITE_API_KEY'], }}> <App /> </ReactTogether> </StrictMode>, )
// Before import { useState } from 'react' export default function Calculator() { const [startingAmount, setStartingAmount] = useState(20000); const [years, setYears] = useState(25); const [returnRate, setReturnRate] = useState(10); const [compoundFrequency, setCompoundFrequency] = useState("annually"); const [additionalContribution, setAdditionalContribution] = useState(500); const [contributionTiming, setContributionTiming] = useState("beginning"); const [contributionFrequency, setContributionFrequency] = useState("month"); // ... }
The beauty of this approach is that the application continues to work exactly as before - the only difference is that now the state updates are synchronized across all connected users.
The final step is adding a way for users to create, join, and leave collaborative sessions. I chose to implement this through a header section above the calculator, making session controls easily visible to everyone.
React Together makes this straightforward by providing four essential hooks:
Here's a simplified version of the header component (I just removed the class names):
// After import { useStateTogether } from 'react-together' export default function Calculator() { const [startingAmount, setStartingAmount] = useStateTogether("startingAmount", 20000); const [years, setYears] = useStateTogether("years", 25); const [returnRate, setReturnRate] = useStateTogether("returnRate", 10); const [compoundFrequency, setCompoundFrequency] = useStateTogether("compoundFrequency", "annually"); const [additionalContribution, setAdditionalContribution] = useStateTogether("additionalContribution", 500); const [contributionTiming, setContributionTiming] = useStateTogether("contributionTiming", "beginning"); const [contributionFrequency, setContributionFrequency] = useStateTogether("contributionFrequency", "month"); // ... }
With this implementation, users can now start collaborative sessions with just a click. When someone joins using the shared URL, they'll immediately see the same state as everyone else, with all changes synchronized in real-time across all participants.
And that's it, it's easy and it just works! Plus you can do it in less than 10 minutes!!
While the basic synchronization worked well, something felt off: elements were changing "by themselves" on the page, with no indication of who was making the changes. This is a common challenge in collaborative applications, and tools like Google Docs solve it by showing where other users are viewing and editing.
True collaboration isn't just about synchronizing state—it's about creating a sense of presence. Users need to "see" each other to work together effectively.
I initially considered to implement shared cursors, letting users see each other's mouse pointers. However, this approach presents challenges in responsive web applications:
Instead, I focused on what we really want to achieve with user presence:
The solution? Highlight the elements that users are interacting with. This approach is simpler, more intuitive, and works reliably across all viewport sizes. Let's see how to implement this in two key areas: chart tabs and input fields.
Let's start with a simple implementation of user presence: showing which users are viewing each chart tab.
For this, we need a special kind of shared state where each user can have their own value that's visible to everyone else.
React Together provides exactly what we need with the useStateTogetherWithPerUserValues hook (yes, that's quite a mouthful!). This hook works similarly to useStateTogether, but instead of sharing a single value, it allows each user to have their own value that's visible to all participants. The hook returns three elements:
Here's how we implement this to show user avatars next to tabs:
// src/index.tsx import { StrictMode } from 'react' import { ReactTogether } from 'react-together' import { createRoot } from 'react-dom/client' import App from './App.tsx' import './index.css' createRoot(document.getElementById('root')!).render( <StrictMode> <ReactTogether sessionParams={{ appId: import.meta.env['VITE_APP_ID'], apiKey: import.meta.env['VITE_API_KEY'], }}> <App /> </ReactTogether> </StrictMode>, )
In the code snippet above, we replaced a useState with a useStateTogetherWithPerUserValues, and once again, the application kept working as it was before, but now everyone could see everyone else's state! Then we just needed to render the new information we just got.
This implementation shows user avatars next to each tab, making it clear which users are viewing which charts. We filter out the current user's avatar to avoid redundancy, as users don't need to see their own presence indicator.
Adding presence indicators to input fields follows a similar pattern to the previous example, but with an additional requirement: we need to track when users start and stop editing. Fortunately, Ant Design's components provide the necessary callbacks for this purpose.
For each input field, I wanted to:
Here's how we implement this using the useStateTogetherWithPerUserValues hook:
// src/index.tsx import { StrictMode } from 'react' import { ReactTogether } from 'react-together' import { createRoot } from 'react-dom/client' import App from './App.tsx' import './index.css' createRoot(document.getElementById('root')!).render( <StrictMode> <ReactTogether sessionParams={{ appId: import.meta.env['VITE_APP_ID'], apiKey: import.meta.env['VITE_API_KEY'], }}> <App /> </ReactTogether> </StrictMode>, )
Although the code is slightly longer, the principle is simple: We just need to track which users are editing each input field, and then render whichever visualization we want.
This same approach works for any other input type, such as drop downs and slider bars!!
--
And that's it! With this fully collaborative investment return simulator it'll be easier for my friend to explain to his son how investment returns worked over Zoom. Mission accomplished! ✨
Looking at how easy it is to create this kind of collaborative website makes me wonder how can the internet bring us closer together when we're online... More on that later!
Hope you learned something new, and feel free to reach out if you have any feedback or questions!!
Happy coding! ?
The above is the detailed content of Add Real-Time Collaboration to Your React App in Minutes. For more information, please follow other related articles on the PHP Chinese website!