Skip to content

WebNinjaDeveloper.com

Programming Tutorials




Menu
  • Home
  • Youtube Channel
  • Official Blog
  • Nearby Places Finder
  • Direction Route Finder
  • Distance & Time Calculator
Menu

Build a Google Docs Clone in React.js & Socket.io Using Quill Wysiwyg Editor & MongoDB in Node.js

Posted on December 24, 2022

 

 

Welcome folks today in this blog post we will be building a google docs clone in react.js and node.js using quill wysiwyg editor & socket.io. All the full source code of the application is shown below.

 

 

Live Demo

 

 

You can see the live demo of the google docs clone as shown below

 

 

 

 

 

Get Started

 

 

In order to get started you need to make a new directory called googledocs using the below command

 

 

mkdir googledocs

 

 

And now you need to make a client folder and inside which we will be creating the react.js project as shown below

 

 

mkdir client

 

 

cd client

 

 

And now you need to create the react.js project using the below command

 

 

npx create-react-app docsapp

 

 

cd docsapp

 

 

And now you need to install the below libraries using the npm command inside react.js project as shown below

 

 

npm i react-quill

 

 

npm i react-router-dom

 

 

npm i uuid

 

 

npm i socket.io-client

 

 

And now you will see the below directory structure of the react.js app as shown below

 

 

 

 

 

And now you need to edit the App.js file and copy paste the following code

 

 

App.js

 

 

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import TextEditor from "./TextEditor"
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Redirect,
} from "react-router-dom"
import { v4 as uuidV4 } from "uuid"
 
function App() {
  return (
    <Router>
      <Switch>
        <Route path="/" exact>
          <Redirect to={`/documents/${uuidV4()}`} />
        </Route>
        <Route path="/documents/:id">
          <TextEditor />
        </Route>
      </Switch>
    </Router>
  )
}
 
export default App

 

 

And now as you can see in the above code we are importing the react-router and also we are importing the switch and redirect and route methods. And then we are initializing the react router and inside that we are using the switch statement to add two routes and inside it we have the parent route in which we are redirecting the user to a random document using the uuidV4() method. And in the second route we are showing the quill editor when the random document is loaded.

 

 

Displaying the Quill Wysiwyg Editor

 

 

Now we need to create the TextEditor.js component file and copy paste the following code

 

 

TextEditor.js

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { useCallback, useEffect, useState } from "react";
import Quill from "quill";
import "quill/dist/quill.snow.css";
import { io } from "socket.io-client";
 
const TOOLBAR_OPTIONS = [
  [{ header: [1, 2, 3, 4, 5, 6, false] }],
  [{ font: [] }],
  [{ list: "ordered" }, { list: "bullet" }],
  ["bold", "italic", "underline"],
  [{ color: [] }, { background: [] }],
  [{ script: "sub" }, { script: "super" }],
  [{ align: [] }],
  ["image", "blockquote", "code-block"],
  ["clean"],
];
 
export default function TextEditor() {
 
 
  const [socket, setSocket] = useState();
  const [quill, setQuill] = useState();
 
  useEffect(() => {
    const s = io("http://localhost:3001");
    setSocket(s);
 
    return () => {
      s.disconnect();
    };
  }, []);
 
  const wrapperRef = useCallback((wrapper) => {
    if (wrapper == null) return;
 
    wrapper.innerHTML = "";
 
    const editor = document.createElement("div");
 
    wrapper.append(editor);
 
    const q = new Quill(editor, {
      theme: "snow",
      modules: { toolbar: TOOLBAR_OPTIONS },
    });
 
    q.disable();
    q.setText("Loading...");
    setQuill(q);
 
  }, []);
 
  return <div className="container" ref={wrapperRef}></div>;
}

 

 

As you can see we are connecting to the socket.io server using the port 3001. And then we are declaring the useState variables for the socket and quill editor. And also we are declaring the options for the quill editor which includes advanced utilities for bolding the text and also supporting for adding the image. And then we are using the useCallback hook to embed the quill editor. And we are using the snow theme for the quill editor. And then we are also attaching the toolbar using the modules property.

 

 

Styling the Editor

 

 

Now guys just copy paste the below css styles inside the styles.css file to style the quill editor

 

 

styles.css

 

 

CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
*, *::before, *::after {
  box-sizing: border-box;
}
 
body {
  background-color: #F3F3F3;
  margin: 0;
}
 
.container .ql-editor {
  width: 8.5in;
  min-height: 11in;
  padding: 1in;
  margin: 1rem;
  box-shadow: 0 0 5px 0 rgba(0, 0, 0, .5);
  background-color: white;
}
 
.container .ql-container.ql-snow {
  border: none;
  display: flex;
  justify-content: center;
}
 
.container .ql-toolbar.ql-snow {
  display: flex;
  justify-content: center;
  position: sticky;
  top: 0;
  z-index: 1;
  background-color: #F3F3F3;
  border: none;
  box-shadow: 0 0 5px 0 rgba(0, 0, 0, .5);
}
 
@page {
  margin: 1in;
}
 
@media print {
  body {
    background: none;
  }
 
  .container .ql-editor {
    width: 6.5in;
    height: 9in;
    padding: 0;
    margin: 0;
    box-shadow: none;
    align-self: flex-start;
  }
 
  .container .ql-toolbar.ql-snow {
    display: none;
  }
}

 

 

 

Loading the Content of New Document in Quill Editor

 

 

Now we will be using the useParams from the react-router-dom library and we will be extracting the documentId

 

 

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useParams } from "react-router-dom";
 
const SAVE_INTERVAL_MS = 2000;
 
export default function TextEditor() {
 
 
  const { id: documentId } = useParams();
  const [socket, setSocket] = useState();
  const [quill, setQuill] = useState();
 
  useEffect(() => {
    if (socket == null || quill == null) return;
 
    socket.once("load-document", (document) => {
      quill.setContents(document);
      quill.enable();
    });
 
    socket.emit("get-document", documentId);
  }, [socket, quill, documentId]);
 
}

 

 

As you can see we are extracting the documentid from the url and then we are sending or emitting the id to the socket.io at the server side to store the document and there we will be joining the room. And then we are receiving the event which is called load-document inside which we are setting the content of the quill editor.

 

 

Sending & Receiving Realtime Changes in Editor

 

 

Now guys we will be writing the code of socket.io where we will be sending realtime changes of the editor to the socket.io at the server side so that the changes made by the concurrent users are reflected back in the browser.

 

 

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
useEffect(() => {
    if (socket == null || quill == null) return;
 
    const handler = (delta) => {
      quill.updateContents(delta);
    };
    socket.on("receive-changes", handler);
 
    return () => {
      socket.off("receive-changes", handler);
    };
  }, [socket, quill]);
 
 
 
  useEffect(() => {
    if (socket == null || quill == null) return;
 
    const handler = (delta, oldDelta, source) => {
      if (source !== "user") return;
      socket.emit("send-changes", delta);
    };
    quill.on("text-change", handler);
 
    return () => {
      quill.off("text-change", handler);
    };
  }, [socket, quill]);

 

 

As you can see we have two useState hooks and inside that we first of all fetching the content from the quill editor and then sending those changes to the socket.io server side using the text-change event inside the quill-editor and then we are emitting the event from client to server using the send-changes and similarly we are receiving the changes using the receive-changes event and then we are updating the contents of the quill editor using the updateContents() method.

 

 

Saving the Contents of Editor in MongoDB

 

 

Now guys we will be automatically saving the contents of the quill editor after 2 seconds. For this we will be sending the event to the socket.io at the server side where the content will be saved to mongodb.

 

 

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
const SAVE_INTERVAL_MS = 2000;
useEffect(() => {
    if (socket == null || quill == null) return;
 
    const interval = setInterval(() => {
      socket.emit("save-document", quill.getContents());
    }, SAVE_INTERVAL_MS);
 
    return () => {
      clearInterval(interval);
    };
  }, [socket, quill]);

 

 

As you can see we have declared the constant variable where we are storing the minimum time after which we are storing the contents inside the mongodb. And for this we are sending the save-document event to the socket.io at the server side. And inside this we are first of all getting the contents of the quill editor using the getContents() method.

 

 

Full Source Code of App.js

 

 

App.js

 

 

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { useCallback, useEffect, useState } from "react";
import Quill from "quill";
import "quill/dist/quill.snow.css";
import { io } from "socket.io-client";
import { useParams } from "react-router-dom";
 
const SAVE_INTERVAL_MS = 2000;
 
const TOOLBAR_OPTIONS = [
  [{ header: [1, 2, 3, 4, 5, 6, false] }],
  [{ font: [] }],
  [{ list: "ordered" }, { list: "bullet" }],
  ["bold", "italic", "underline"],
  [{ color: [] }, { background: [] }],
  [{ script: "sub" }, { script: "super" }],
  [{ align: [] }],
  ["image", "blockquote", "code-block"],
  ["clean"],
];
 
export default function TextEditor() {
 
 
  const { id: documentId } = useParams();
  const [socket, setSocket] = useState();
  const [quill, setQuill] = useState();
 
  useEffect(() => {
    const s = io("http://localhost:3001");
    setSocket(s);
 
    return () => {
      s.disconnect();
    };
  }, []);
 
 
 
  useEffect(() => {
    if (socket == null || quill == null) return;
 
    socket.once("load-document", (document) => {
      quill.setContents(document);
      quill.enable();
    });
 
    socket.emit("get-document", documentId);
  }, [socket, quill, documentId]);
 
 
 
  useEffect(() => {
    if (socket == null || quill == null) return;
 
    const interval = setInterval(() => {
      socket.emit("save-document", quill.getContents());
    }, SAVE_INTERVAL_MS);
 
    return () => {
      clearInterval(interval);
    };
  }, [socket, quill]);
 
 
 
  useEffect(() => {
    if (socket == null || quill == null) return;
 
    const handler = (delta) => {
      quill.updateContents(delta);
    };
    socket.on("receive-changes", handler);
 
    return () => {
      socket.off("receive-changes", handler);
    };
  }, [socket, quill]);
 
 
 
  useEffect(() => {
    if (socket == null || quill == null) return;
 
    const handler = (delta, oldDelta, source) => {
      if (source !== "user") return;
      socket.emit("send-changes", delta);
    };
    quill.on("text-change", handler);
 
    return () => {
      quill.off("text-change", handler);
    };
  }, [socket, quill]);
 
 
 
  const wrapperRef = useCallback((wrapper) => {
    if (wrapper == null) return;
 
    wrapper.innerHTML = "";
 
    const editor = document.createElement("div");
 
    wrapper.append(editor);
 
    const q = new Quill(editor, {
      theme: "snow",
      modules: { toolbar: TOOLBAR_OPTIONS },
    });
 
    q.disable();
    q.setText("Loading...");
    setQuill(q);
 
  }, []);
 
 
  return <div className="container" ref={wrapperRef}></div>;
 
}

 

 

Making the Socket.io Backend in Node.js

 

 

Now guys we will be making the backend for this app. Just create the server folder and execute the below commands as shown below

 

 

mkdir server

 

 

cd server

 

 

And now we need to install the below libraries as shown below

 

 

npm i socket.io

 

 

npm i mongoose

 

 

And now just see the directory structure of the backend is shown below

 

 

 

 

 

 

Making the Database in MongoDB

 

 

Now guys first of all you need to make a new database using the mongodb community edition as shown below

 

 

 

 

 

 

 

Connecting to MongoDB Database

 

 

Now guys just create the server.js file which will be main file of the backend and copy paste the following code

 

 

server.js

 

 

JavaScript
1
2
3
4
5
6
7
8
9
const mongoose = require("mongoose")
const Document = require("./Document")
 
mongoose.connect("mongodb://localhost:27017/google-docs-clone", {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useFindAndModify: false,
  useCreateIndex: true,
})

 

 

As you can see we are connecting to the mongodb database at the port number 27017 where mongodb service typically runs in the computer. And then we are providing the database name that we have created in the earlier step. And also we are including the schema file for the document model which can be defined as shown below

 

 

Document.js

 

 

JavaScript
1
2
3
4
5
6
7
8
const { Schema, model } = require("mongoose")
 
const Document = new Schema({
  _id: String,
  data: Object,
})
 
module.exports = model("Document", Document)

 

 

 

Making the Socket.io Server

 

 

Now guys we will be making the socket.io server where we will be receiving and sending the events from the client side.

 

 

JavaScript
1
2
3
4
5
6
const io = require("socket.io")(3001, {
  cors: {
    origin: "http://localhost:3000",
    methods: ["GET", "POST"],
  },
})

 

 

As you can see we are connecting to the 3000 port where our client side react app lives. And for this we are using the cors module to connect to that origin and we are allowing the get and post methods on that origin. And we are starting this socket.io server at the port 3001.

 

 

Sending and Receiving Events in Socket.io

 

 

Now guys we will be seeing how basically we can receive the events which are sent by the client and also send events to the client side from the backend in socket.io

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const defaultValue = ""
 
io.on("connection", socket => {
  socket.on("get-document", async documentId => {
    const document = await findOrCreateDocument(documentId)
    socket.join(documentId)
    socket.emit("load-document", document.data)
 
    socket.on("send-changes", delta => {
      socket.broadcast.to(documentId).emit("receive-changes", delta)
    })
 
    socket.on("save-document", async data => {
      await Document.findByIdAndUpdate(documentId, { data })
    })
  })
})

 

 

As you can see guys we are connecting to the random room using the documentId that we receive from the client side. Basically we join the rooms inside socket.io using the join() method And then we are sending the load-document event to the client and also we are sending the default value for the document. And also all this code is wrapped inside the get-document event that we receive from the client side and here we are listening that event. And for the changes we are listening for send-changes event here we are broadcasting that event to all the clients connected in the same room except the client.

 

And lastly for saving the document inside the mongodb database we are using the findByIdAndUpdate() of mongodb database to store the content of the editor. Now we need to define the method where we select the document using it’s id.

 

 

JavaScript
1
2
3
4
5
6
7
async function findOrCreateDocument(id) {
  if (id == null) return
 
  const document = await Document.findById(id)
  if (document) return document
  return await Document.create({ _id: id, data: defaultValue })
}

 

 

As you can see we are receiving the documentId inside this function. If the documentId is not found then we are creating the brand new document using the create() method and insert the info inside mongodb and if the documentId is found then we are simply returning the document back to the client.

 

 

Starting the MERN Stack App

 

 

Now guys we can start both the client and the backend app as shown below

 

 

starting react.js app

 

npm start

 

 

starting backend

 

 

nodemon server.js

 

 

Recent Posts

  • Build a JWT Login & Registration Auth System in Node.js & Express Using MongoDB in Browser
  • React-Admin Example to Create CRUD REST API Using JSON-Server Library in Browser Using Javascript
  • Javascript Papaparse Example to Parse CSV Files and Export to JSON File and Download it as Attachment
  • Javascript Select2.js Example to Display Single & Multi-Select Dropdown & Fetch Remote Data Using Ajax in Dropdown
  • Video.js Video Player Plugin Library in Javascript For Playing Videos in Browser
  • Angular
  • Bunjs
  • C#
  • Deno
  • django
  • Electronjs
  • java
  • javascript
  • Koajs
  • kotlin
  • Laravel
  • meteorjs
  • Nestjs
  • Nextjs
  • Nodejs
  • PHP
  • Python
  • React
  • ReactNative
  • Svelte
  • Tutorials
  • Vuejs




©2023 WebNinjaDeveloper.com | Design: Newspaperly WordPress Theme