Engineering | 06 March, 2023
Building Realtime Editor with TipTap for your NextJS project

Varun Raj / 06 March, 2023
Realtime editors like Google Docs and Notion have become an essential tool for collaboration in many teams and organizations. These editors allow multiple users to work on the same document simultaneously, making it easier for teams to collaborate on projects and share feedback in real-time.
These editors are particularly important for remote teams, as they help to reduce communication delays and ensure that all team members have access to the most up-to-date version of a document.
What we need?
If you're considering using a Realtime Editor for your Next.js project, you'll need to choose the right tools to ensure smooth and efficient collaboration. Here are the tools we’ll be seeing in this article.
Next.js - For building the web application
TipTap - For building the Rich Text Editor
HocusPocus - For adding realtime capabilities in TipTap
The Setup
To set up your Next.js web app with Tiptap and HocusPocus, we’ll start with a boilerplate, install Tiptap with its starter kit, and then install the HocusPocus provider and server.
Setup Web App using Next.js
Clone the Next.js Boilerplate repository from https://github.com/ixartz/Next-js-Boilerplate.
git clone [https://github.com/ixartz/Next-js-Boilerplate](https://github.com/ixartz/Next-js-Boilerplate) realtime-editor
cd realtime-editor
yarn install
Start the development server using
yarn run dev
Open http://localhost:3000 in your web browser to see the sample page.
Now lets add a page for rendering our editor
import React from 'react'
export default function EditorPage() {
return <div>EditorPage</div>
}
Install and Setup of TipTap Editor
Install the TipTap package along with its dependenices
yarn add @tiptap/react @tiptap/pm @tiptap/starter-kit
Create a new component called RichTextEditor under components folder
import { EditorContent, useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import React from 'react';
export default function RichTextEditor() {
const editor = useEditor({
extensions: [StarterKit],
content: '<p>Hello World! 🌎️</p>',
});
return <EditorContent editor={editor} />;
}
And import it to our EditorPage
import React from 'react'
import RichTextEditor from '../components/RichTextEditor'
export default function EditorPage() {
return <RichTextEditor />
}
Adding some styling to the editor as it looks plain by default, we can use @tailwindcss/typography
install the dependency in your project
yarn add -D @tailwindcss/typography
Add the plugin to the tailwind config tailwind.config.js
module.exports = {
// ...
plugins: [
require('@tailwindcss/typography'),
// ...
],
}
Add the prose
and other styling classes to your editor and restart the app
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'
export default function RichTextEditor() {
const editor = useEditor({
extensions: [StarterKit],
content: '<p>Hello World! 🌎️</p>',
editorProps: {
attributes: {
class:
'prose dark:prose-invert prose-sm sm:prose-base lg:prose-lg xl:prose-2xl m-5 focus:outline-none'
}
}
})
return <EditorContent editor={editor} />
}
Once loaded you’ll be able to see the styling for all the different elements
Installing HocusPocus and Integrating with Editor
HocusPocus is a realtime collaborative plugin by Tiptap team. You can use it in your application by setting up the server
and the provider
Setting up server
The hocus pocus server is a websocket server which we need to run as a separate application, we’ll create the server code in the same project but while running we can run separately.
yarn add @hocuspocus/server --save
Now create a new file server/hocuspocus-server.js
and add the following code
const { Hocuspocus } = require('@hocuspocus/server')
// Configure the server …
const server = new Hocuspocus({
port: 1234
})
// … and run it!
server.listen()
Lets run the web socket server with node directly
node server/hocuspocus-server.js
Setup of Hocuspocus Provider
Now that we’ve our websocket server running, lets install and connect the hocuspocus provider in the editor
yarn add @hocuspocus/provider @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor yjs y-webrtc y-prosemirror
After installation we need to do three things,
Create a y doc
const ydoc = new Y.Doc()
Create a provider instance
const provider = new HocuspocusProvider({
url: 'ws://127.0.0.1',
name: 'example-document',
document: ydoc
})
Setup the plugins
Collaboration.configure({
document: ydoc,
}),
CollaborationCursor.configure({
provider,
user: { name: 'John Doe', color: '#ffcc00' },
}),
The overall RichTextEditor component will look like
import { HocuspocusProvider } from '@hocuspocus/provider'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'
import * as Y from 'yjs'
export default function RichTextEditor() {
const ydoc = new Y.Doc()
const provider = new HocuspocusProvider({
url: 'ws://127.0.0.1:1234',
name: 'example-document',
document: ydoc
})
const editor = useEditor({
extensions: [
StarterKit,
Collaboration.configure({
document: ydoc
}),
CollaborationCursor.configure({
provider,
user: { name: 'John Doe', color: '#ffcc00' }
})
],
content: '<p>Hello World! 🌎️</p>',
editorProps: {
attributes: {
class:
'prose dark:prose-invert prose-sm sm:prose-base lg:prose-lg xl:prose-2xl m-5 focus:outline-none'
}
}
})
return <EditorContent editor={editor} />
}
But when you try to run it, it wont work due to SSR Limitation. Because when the SSR is. generated the editor wont have Websocket instance.
In order to solve this we need to load the editor only in the client side. So in the editor.tsx page convert the import of RichTextEditor
to Dynamic import without SSR
import dynamic from 'next/dynamic'
import React from 'react'
const RichTextEditor = dynamic(() => import('../components/RichTextEditor'), {
ssr: false
})
export default function EditorPage() {
return <RichTextEditor />
}
Finally add some styling for the collaborator’s labels in global.css
.collaboration-cursor__caret {
border-left: 1px solid #0d0d0d;
border-right: 1px solid #0d0d0d;
margin-left: -1px;
margin-right: -1px;
pointer-events: none;
position: relative;
word-break: normal;
}
.collaboration-cursor__label {
border-radius: 3px 3px 3px 0;
color: #0d0d0d;
font-size: 12px;
font-style: normal;
font-weight: 600;
left: -1px;
line-height: normal;
padding: 0.1rem 0.3rem;
position: absolute;
top: -1.4em;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
white-space: nowrap;
}
We’re set now, if you load the page in two browsers you’ll see the magic of realtime editing.
Screen Recording 2023-03-05 at 4.45.20 PM.mov
In the future articles in this series I will discuss more in depth concepts of realtime editing like double renders, storing the document in database and also initial load.
Last updated: September 7th, 2023 at 7:42:06 AM GMT+0

Varun Raj
Varun is one of the founders of Hellonext. He has helped companies like Google build large-scale developer communities to build strong developer relationships. He has build over 50 SaaS products in his career.