A beginners’ guide of implementing co-operative editing document using React and socket.io
1. Tech-Stack:
- React (create-react-app)
- Koa
- Socket.io
- nodemon for HMR on server side (optional)
(You need Node and npm/yarn installed, as well as some basic webpack knowledge)
2. Abstraction
This App is a simple implementation of co-operative editing document meant for demonstrating how client-ui can work with server through a constant connection called websocket.
The client-UI is based of Flavio Copes(https://flaviocopes.com/) React Excel tutorial,
https://flaviocopes.com/tutorial-react-spreadsheet/
while this guide emphasize on how socket.io works to establish constant connection
between multiple clients and server.
3. Folders Structure
**Client Side **(react create-react-app) Client’s name folder
src
Table
-> Table.js
Cell
-> Cell.js
Row
-> Row.js
App.js
index.js
Server Side (Koa2) Server(API) name folder
server.js
4. The Client side => To sum up: defining what happen next in each event’s callback
The code before Introducing Formula is an Excel table with editing function without Math(all the code above Introducing Formula), and we will try to make it real-time like Google Docs. When it’s done, your code should be similar to https://flaviocopes.com/tutorial-react-spreadsheet/ Before Introducing Formula.
In order to make real-time co-operative edit work, we need to add some events in componentDidMount .
We will add these events:
- sheet’s Content changing
- sheet editing (start and stop)
- Content Changed Event
When the change of table’s content is changed we will emit an event to notify the server, and the other client.
We define the function sendMessage anywhere below the “sonstructor” method and above the render() in Table.js
and call “sendMessage” in “handleChangedCell” as this function handle the total table data and apply to each cell.
1 | handleChangedCell = ({ x, y }, value) => { |
By doing this we send the table data along with sender’s name to backend, and backend will respond and transmit a new singal(‘sknew’).
socket.broadcast.emit
“socket.broadcast.emit() “ can also send the data to everyone except the sender.
1 | socket.on('skchange', data => { |
which is the same as
1 | // users {:Map username => id} |
The Table.js has a state “data” which is essentially the data of each column and row stored in one object.
When one client changed data, we want another client to have the same data.
So we just Transmit the whole data, through server and to another client.
Tips!:we will do this in componentDidmount()
1 | componentDidMount() { |
with the server code (shown above)
we shall have the app working like this

Now we have the real-time text, let’s make others know who is editing.

To do that, we need to send out socket signals of “Start Editing” and “End Editing” at “double-clicked” “onBlur” respectively.
1 | doubleClicked = () => { |
Above is how we send the “Start Editing”, and we need to code the receiving signal for this.
Since we want this to happen after the DOM is mounted, we code the behavior on receiving events in componentDidmount()
1 | socket.on('skeditcoming', (data) => { |
we transfer the data including: the guy’s name who is editing, which cell is editing, and the state for editing.
After we finished the “Start Editing” signal, let’s get to the “End Editing”
1 | onBlur = (e) => { |
And the receiving part.
1 | socket.on('editendcoming', (data) => { |
SUM UP: Every event we register will have two different names come back and forth,
If we emit “newmessage” to server, after server received it, it need another name to send to client “newMessageComing”

5. Transmitting data through Socket.io
The server side is simplified to demonstrate how socket.io works with http server and client-UI with no touch on database.
First, we create the instance of websocket using Koa.
Note that Koa requires app.callback() as parameter
1 | const server = require('http').createServer(app.callback()) |
which is unlike express
1 | var http = require('http').Server(app); |
Next, in my implementation I used a map collection to record the current logged in users.
1 | let users = new Map() |
I personally chose a customized signal to record each user’s information (username, socketId, etc..)
In client side, where login event will trigger this function. Emitting the “getID” signal while server-side store and handle the users’ information.
1 | handleLogin = () => { |
Some controversies here:
The other way around is transmitting information in the callback function of on(“connect”),
which means transmitting data over the first/short TCP “touching base” request, seems unstable? But it’s doable and is in the official docs.
But I found it unstable with the sequence of setting other state in client side or with other ajax action which required the use of Async decorator to achieve desired code firing order.
Let’s also set a customized disconnect event for client, so the global users map object can now update according to users’ login/logout action with unique socketID.
users map delete the socketID of disconnected users using map.prototype.delete(), easy right?
Put this block of code inside the scope of io.on(‘connection’){} as this is a customized event.
1 | socket.on('disconnect', ()=>{ |
Tips:
In fact, all of the customized events need to be put in the scope of
1 | io.on("connection", (socket)=>{ |
Adding the events of cooperative document will use,
- sheet changing
- sheet editing
The sheet can be now edited while another client knowing it.
Here is what “server/server.js” file will look like
1 | var Koa = require('koa'); |
6. References
The react-sheet-ui with no websocket enhanced is based off Flavio Copes(https://flaviocopes.com/) React Excel tutorial.