Engineering | 06 March, 2023

Building Realtime Editor with TipTap for your NextJS project

Hellonext, Inc.

Varun Raj

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.

Realtime Changelog Editor Demo

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 />
}

Realtime Demo

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

Realtime Demo

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

Realtime Demo

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.

Realtime Demo

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

Hellonext Blogpost Author Profile

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.