Making A Simple Real-Time Collaboration App with React, Node, Express, and Yjs
I want to make a simple collaborative text editing app that can be accessed by two people at the same time in the browser.
Version One (Part 1)
To start, I’ll make a React app. My App.js file renders a container, TextEditContainer, that renders a TextEdit component that looks like this:
import React, { Component } from 'react';class TextEdit extends Component {
render() {
return (
<div> <h3>
Here's the TextEdit component.
</h3> <textarea
rows="10" cols="50"
placeholder="Write something here..."
>
</textarea> </div>
);
}
}export default TextEdit;
Opening the app by running two different servers and viewing it from those two different ports will, naturally, show the same default app to two different users. The user viewing the app at localhost:3000 can type in the textarea, and the user viewing the app at localhost:3001 can type in the textarea, and neither user will see what the other user is typing.
That’s great for most apps, but I want this app to be collaborative. In other words, I want user2 to see the same text as user1, and vice versa, from the same port.
So far, I haven’t introduced any state into this app. My TextEditContainer and TextEdit are both presentational components.
My intention is to build this as a simple frontend app, meaning no backend, no database, no persistence on browser refresh.
Of course, ‘simple’ doesn’t mean ‘easy’.
For example, “Never eat candy” is a simple directive, but it isn’t easy to follow, especially if you’ve got a sweet tooth like me.
After some research, I realized I’d need a signaling server to get two or more browsers talking to each other. The below quote is from this WebRTC chat app tutorial, which summarizes this situation nicely:
We won’t do video or audio — we’ll simply use WebRTC as a convenient way of sending chat messages. The benefit of this is we won’t need to build a backend to relay messages between clients. Instead, we can handle it entirely client-side in the browser. Convenient, isn’t it? There’s a tiny hiccup in the no-backend plan though. To connect two people over WebRTC, we first need to exchange information to allow browsers to talk to each other. This process is called signaling. …We’ll set up a tiny signaling server with Node.js to deal with this. Other than signaling, no data needs to be sent through a backend!
OK. So I’m going to Socket.io to my app, which, according to their website, “enables real-time bidirectional event-based communication.”
Normally I would separate my server code and client code into two separate directories, but in this case, I’m going to include everything within my React app folder.
I read a bunch of blog posts before getting this part of my process from this tutorial: “Combining React with Socket.io for real-time goodness”. Mmm, real-time goodness. As the tutorial says, I first installed Socket.io:
npm i --save socket.io
I can see that updated my package.json file with a new dependency. Nice.
Next I made a server.js file in my root folder, and added the following:
const io = require('socket.io')();io.on('connection', (client) => {
//here can start emitting events to the client
client.on('subscribeToTimer', (interval) => {
console.log('client is subscribing to timer with interval ', interval);
setInterval(() => {
client.emit( 'timer', new Date() );
}, interval);
});
})const port = 8000;
io.listen(port);
console.log('server.js - listening on port: ', port);Cool.
Cool. Again, I’m following along with that tutorial, varying it slightly based on my React app’s file structure.
Next I created an api folder, and gave it an index.js file, which contained the following:
import openSocket from 'socket.io-client';
const socket = openSocket('http://localhost:8000');export function subscribeToTimer(callback) {
socket.on('timer', timestamp => callback(null, timestamp));
socket.emit('subscribeToTimer', 1000);
}
Finally I updated my TextEditContainer, importing all the api calls into it, making it a stateful component, and called my new subscribeToTimer function within it. I also added some fancy formatting to that unix timestamp, so it would be presented to the user in a nicer date and time format.
import React, { Component } from 'react';import * as api from '../api'import TextEdit from '../components/TextEdit.js';class TextEditContainer extends Component {
constructor() {
super()
this.state = {
timestamp: 'no timestamp yet',
text: ''
};
}componentDidMount() {
api.subscribeToTimer((err, timestamp) => this.setState({
timestamp
}));
}render() {
return (
<div> <h2>
Here's the TextEditContainer.
</h2> <p>
Time: {this.state.timestamp ? new Date(this.state.timestamp).toLocaleString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: true,
timeZoneName: 'short'
}) : "no date yet"}
</p> <TextEdit
text={this.state.text}
/> </div>
);
}
}export default TextEditContainer;
Then I ran my node server:
node server
And I ran a local server to see my React app:
npm start
Which gave me:
A clock.
Cool.
My goal was to make a collaborative text editor, and I ended up with a clock.
But this is progress! My React app now has web sockets installed via Socket.io, as well as a small Node server that is emitting events to the client, AKA the React app in the browser. Right now the only event my frontend is subscribing to is a timer, but I can make my app subscribe to any event.
Version One (Part 2)
I decided to go back to the drawing board, and follow along with Socket.io’s chat app tutorial. Then I refactored the code, first converting the script section from jQuery to Vanilla JavaScript, then changed the page from a form field that adds LIs to a UL onsubmit to a form with a textarea, that adds the text of the textarea to the textarea on keyup. This seems to work!
Will this work with another user on another computer?
No, because my app is localhost, and localhost is local.
To see if I can get it working with a user on a different computer, I need to get it hosted on the good old www. Heroku, here we come.
Thankfully, Heroku has instructions specifically for Using WebSockets on Heroku with Node.js, including specific instructions for doing just that with Socket.io. Awesome! Thank you Heroku, you are the best.
Here’s my Super-Simple Real-Time Collaboration App, version 1, built with Express and Socket.io.
But it’s buggy. Let’s make it better.
Version One (Part 3)
Next step: adding Socket.io and an Express server to a React app, again.
I decided to redo my React app, based on this blog post.
I removed the api folder, and instead simply have a server.js file.
Then in my TextEditContainer, I have the following:
import io from "socket.io-client";const socket = io('localhost:8080');
To initiate the request to open a socket connection with my Express server, in server.js.
This got my React app working. I could have two users, from two different browser windows, edit the same textarea.
But the editing experience is pretty shitty, and buggy, with lots of lag time. This Convergence Labs blog post explained the situation well:
When you set the value of a text area, the local user’s selection is cleared and their cursor position (and scroll) is moved to the end of the text area, disrupting any typing they may have been doing... and we haven’t even considered things like shared cursors and selections.
Definitely not a seamless, Google Docs-like experience. So it was time for more googling, and more research.
Version Two
I read more about the difference between OT and CRDTs.
I reread some blog posts, like Real-Time React with Socket.io: Building a Pair Programming App and Building Conclave: a decentralized, real time, collaborative text editor.
I looked into what solutions were made by other people “looking for a library that would allow me to synchronize text in real-time between multiple users (ala Google Docs).”
Eventually I decided to focus on using Yjs: “a framework for real-time p2p shared editing on any data”. I had a little trouble getting started with it, but soon enough had an app working locally.
This YouTube tutorial helped me get started: How to build an Collaborative Editing Application with IPFS using CRDT, as did the Yjs documentation.
I used Yjs’s Browser/Node client and its Node server. Since Yjs’s websocket connector is build on top of Socket.io, my previous experience building out apps with Socket.io didn’t go to waste. I looked through the functions I was installing and requiring from Yjs, and had some understanding of what they were doing.
Soon my collaborative, real-time text editor, built with Node, Express, and Yjs, was working locally. Then I got it hosted on Heroku, so it worked online.
The coolest thing about the app is that it works offline too. After disconnected from the internet, a user could make changes, and then upon reconnecting, the changes would be synced with the text online.
Yjs is awesome.
Here’s version 2 of my app: https://textarea-yjs-express.herokuapp.com/
Pretty cool!
Now I just want to create a React frontend for this app.
And away… we… go!
Version Three
Finally, here’s version three of my app, built with React, Express, and Yjs.
Similar to my last app, my React app also has a directory within it that acts as the Yjs-websockets-server, which the Yjs component uses for its connection.
Version Four
Plain text in a simple textarea is fine and all, but it looks a little dry.
Since Yjs supports two-way binding with Quill’s richtext editor, I added Quill to my app. Now users can bold and underline their text. Pretty sweet!
Note I used QuillJS, and not the npm package react-quill.
To style my Quill text editor, I used the ‘snow’ theme, but I couldn’t get the icons to render correctly, so I used free icons from Font Awesome for that.
Here’s the final, final version of my app, built with React, Express, and Yjs.
Conclusion
This took some trial-and-error, missteps, false starts, and return-to-the-drawing-board moments.
At first I thought making a collaborative text editing app would be easy. It proved to be harder that I thought. But once I found the right tools for the job, and took the time to learn how they worked, and how to use them, building this wasn’t too hard at all. The research was as important as the coding.
Thankfully, other people have built apps like this before, and built the tools to make the apps, and made those tools open source. Thank God for open source software!
It’s both humbling and exhilarating to know that I can build out my own version of Google Docs in a few days, thanks to open source software like Yjs.
Thanks for reading, internet nerds! Now go build something cool.