Sign in

Tiptap best practices and tips

This guide covers best practices and common pitfalls to avoid when working with Tiptap and Liveblocks.

Always include StarterKit or Doc & Paragraph extensions

The Tiptap StarterKit extension is required for Tiptap to function properly. It provides essential node types that every Tiptap editor needs, specifically the Doc and Paragraph nodes. Alternatively you can include only the Doc and Paragraph extensions.

Why StarterKit is required

Tiptap documents must have a root Doc node and at least one Paragraph node to maintain a valid document structure. Without these core nodes, the editor will not work correctly and synchronization will fail.

import { useEditor } from "@tiptap/react";import StarterKit from "@tiptap/starter-kit";
const editor = useEditor({ extensions: [ StarterKit.configure({ // Required, Liveblocks extension handles its own history undoRedo: false, }), // Add other extensions here ],});

What StarterKit includes

In addition to the required Doc and Paragraph nodes, StarterKit also includes commonly used extensions like:

  • Text formatting (Bold, Italic, Strike, Code)
  • Block types (Heading, Blockquote, CodeBlock, BulletList, OrderedList, ListItem)
  • Horizontal rule and hard break
  • History (undo/redo) (This must be disabled to work with Liveblocks)

If you need more control over which extensions are included, you can configure StarterKit to disable specific extensions:

const editor = useEditor({  extensions: [    StarterKit.configure({      // Disable extensions you don't need      heading: false,      blockquote: false,      // Required, Liveblocks extension handles its own history      undoRedo: false,    }),  ],});

However, you should never disable the doc or paragraph options, as these are required for the editor to function.

Disable server-side rendering with immediatelyRender: false

When using Tiptap with server-side rendering (SSR) frameworks like Next.js, you should always set immediatelyRender: false in your useEditor hook. Tiptap should never be rendered on the server.

Why disable server-side rendering?

Tiptap is a client-side editor that relies on browser APIs and the DOM. When rendered on the server, it can cause:

  • Hydration mismatches between server and client
  • Errors related to missing browser APIs

How to disable server-side rendering

Set immediatelyRender: false in your useEditor configuration:

// ✅ CORRECT: Disable server-side rendering for Next.js and other SSR frameworksimport { useEditor } from "@tiptap/react";import { useLiveblocksExtension } from "@liveblocks/react-tiptap";import StarterKit from "@tiptap/starter-kit";
function Editor() { const liveblocks = useLiveblocksExtension();
const editor = useEditor({ // Required for SSR frameworks like Next.js immediatelyRender: false, extensions: [ StarterKit.configure({ // Required, Liveblocks extension handles its own history undoRedo: false, }), liveblocks, ], });
return <EditorContent editor={editor} />;}

This ensures that Tiptap is only rendered on the client side, avoiding any server-side rendering issues.

Enable content validation

Tiptap has a schema that defines the structure of your document. By default, Tiptap does not validate content against this schema. When invalid content is present, it will silently break synchronization without any error messages.

This can happen when:

  • Extensions are added or removed from the editor
  • The schema changes between different versions of your application
  • Users collaborate on documents with different editor configurations

How to enable content validation

Always enable content validation by setting enableContentCheck: true and implementing an onContentError handler:

import { useEditor } from "@tiptap/react";import StarterKit from "@tiptap/starter-kit";
const editor = useEditor({ extensions: [ StarterKit.configure({ // Required, Liveblocks extension handles its own history undoRedo: false, }), ], enableContentCheck: true, onContentError: ({ editor, error, disableCollaboration }) => { // Disable collaboration to prevent data corruption disableCollaboration();
// Make the editor read-only to prevent further changes editor.setEditable(false, false);
// Log the error for debugging console.error("Content validation error:", error);
// Notify the user that there's an issue alert( "There was an error loading this document. The content may be incompatible with the current editor version. The document has been made read-only to prevent data loss." ); },});

This ensures that:

  • Invalid content is detected early
  • Collaboration is disabled to prevent data corruption
  • The editor is made read-only to prevent further changes
  • The issue is logged for debugging
  • Users are notified of the problem

Use initialContent instead of content

When setting default content for your Tiptap editor, always use initialContent on useLiveblocksExtension instead of content on useEditor. Using content will cause the content to be appended to the document every time the page loads or the component re-renders.

The problem with content

The content option in Tiptap sets the editor content every time the editor is initialized. When using Liveblocks, this means the content will be added to the existing document rather than replacing it, causing duplication:

// ❌ AVOID: This will duplicate content on every page loadconst editor = useEditor({  extensions: [    StarterKit.configure({      // Required, Liveblocks extension handles its own history      undoRedo: false,    }),    liveblocks,  ],  // ❌ This text will be added to the document every time the editor is loaded  content: "<p>Default text</p>",});

Use initialContent instead

The initialContent option sets a flag internally and only sets the content the very first time the document is empty. This prevents duplication:

// ✅ CORRECT: This only sets content onceimport { useEditor } from "@tiptap/react";import { useLiveblocksExtension } from "@liveblocks/react-tiptap";import StarterKit from "@tiptap/starter-kit";
function Editor() { const liveblocks = useLiveblocksExtension({ // ✅ This text is only set the first time the room is used initialContent: "<p>Default text</p>", });
const editor = useEditor({ extensions: [ StarterKit.configure({ // Required, Liveblocks extension handles its own history undoRedo: false, }), liveblocks, ], });
return <EditorContent editor={editor} />;}

Support multiple editors with the field option

If you want to display multiple Tiptap editors on the same page, use the field option with a unique identifier for each editor. This ensures that each editor synchronizes to its own section of the Yjs document.

import { useEditor } from "@tiptap/react";import { useLiveblocksExtension } from "@liveblocks/react-tiptap";import StarterKit from "@tiptap/starter-kit";
function EditorOne() { const liveblocks = useLiveblocksExtension({ // Unique identifier for this editor field: "editor-1", });
const editor = useEditor({ extensions: [ StarterKit.configure({ // Required, Liveblocks extension handles its own history undoRedo: false, }), liveblocks, ], });
return <EditorContent editor={editor} />;}
function EditorTwo() { const liveblocks = useLiveblocksExtension({ // Different unique identifier field: "editor-2", });
const editor = useEditor({ extensions: [ StarterKit.configure({ // Required, Liveblocks extension handles its own history undoRedo: false, }), liveblocks, ], });
return <EditorContent editor={editor} />;}

Without the field option, both editors would synchronize to the same location in the document, causing conflicts and data loss.

Never use extensions with binary data

Never use Tiptap extensions that store binary data (such as base64-encoded images) directly in the document. Binary data will be synchronized across all clients and can fill up your Liveblocks room extremely quickly, leading to:

  • Increased bandwidth usage
  • Slower synchronization
  • Higher storage costs
  • Potential rate limiting

Common pitfall: Image extension

The official Tiptap Image extension has an allowBase64 option. This option is defaulted to false, and it should never be set to true.

// ❌ NEVER DO THISimport Image from "@tiptap/extension-image";
const editor = useEditor({ extensions: [ StarterKit.configure({ // Required, Liveblocks extension handles its own history undoRedo: false, }), Image.configure({ // This will quickly fill up your room and cause problems allowBase64: true, }), ],});

Recommended approach

Instead of storing images as base64 in the document:

  1. Upload images to a file storage service (e.g., AWS S3, Cloudflare R2, Vercel Blob)
  2. Store only the URL in the document
  3. Reference the URL in your image nodes
// ✅ CORRECT: Store only URLsimport Image from "@tiptap/extension-image";
const editor = useEditor({ extensions: [ StarterKit.configure({ // Required, Liveblocks extension handles its own history undoRedo: false, }), Image.configure({ // The default option. Never set to `true` as it will cause issues with Liveblocks allowBase64: false, }), ],});
// When adding an imageeditor.commands.setImage({ // URL only, no base64 src: "https://your-storage.com/images/photo.jpg",});

Prevent users from losing unsaved changes

To prevent users from accidentally losing their work when closing the browser tab or navigating away, enable the preventUnsavedChanges option:

import { RoomProvider } from "@liveblocks/react/suspense";
<RoomProvider id="my-room" initialPresence={{}} // Warn users before they leave with unsaved changes preventUnsavedChanges={true}> {/* Your components */}</RoomProvider>;

This will display a browser confirmation dialog when users try to leave the page with unsaved changes, helping prevent accidental data loss. Learn more.

We use cookies to collect data to improve your experience on our site. Read our Privacy Policy to learn more.