Skip to main content

Command Palette

Search for a command to run...

Why React-Quill Broke My React 19 App and How TipTap Saved It

A real error I hit while building a fullstack blog editor — and the exact fix that worked.

Published
5 min read
Why React-Quill Broke My React 19 App and How TipTap Saved It
C

Hi, I’m Naydum C. Obia, a web designer and developer passionate about building modern digital experiences and helping brands grow online. I’m the co-founder of Sticobytes, a digital agency that offers web design, development, and tech-driven solutions for businesses and communities. I love exploring how technology, design, and strategy come together to create real impact — especially in rural and emerging markets. 💡 On this space, I share what I learn as I grow in web development, digital entrepreneurship, and the world of modern tech.


Introduction

I'm currently building my first fullstack web application — a digital agency website with a complete blog management system. On Day 13 of the build, I ran into one of those errors that stops everything and sends you deep into Stack Overflow. This is the story of that error, why it happened, and how I fixed it.


What I Was Building

I needed a rich text editor for an admin blog dashboard. The requirements were simple:

  • Bold, italic, headings

  • Bullet lists and numbered lists

  • Code blocks

  • Image support

React-Quill seemed like the obvious choice. It's popular, well-documented, and has thousands of GitHub stars. So I installed it:

npm install react-quill@2.0.0 quill@1.3.7 --legacy-peer-deps

Everything looked fine. Then I opened the page.


The Error

Uncaught TypeError: react_dom_1.default.findDOMNode is not a function
    at ReactQuill2.getEditingArea (react-quill.js:13139:45)
    at ReactQuill2.instantiateEditor (react-quill.js:13028:50)
    at ReactQuill2.componentDidMount (react-quill.js:12998:16)

The editor crashed immediately on mount. The page was blank. Nothing worked.


Why This Happens

The key is in the error: findDOMNode is not a function.

findDOMNode was a React DOM method that allowed class components to access their underlying DOM node. React deprecated it years ago and finally removed it completely in React 19.

React-Quill 2.0.0 internally uses class components and calls findDOMNode to locate its editor container. Since React 19 no longer has this method, the editor crashes the moment it tries to mount.

You can verify your React version by checking package.json:

"react": "^19.2.0"

If you are on React 19, React-Quill will not work. There is no workaround — the issue is inside React-Quill's source code itself.


What I Tried First That Didn't Work

I tried wrapping the editor in an error boundary. The editor still crashed before rendering.

I tried lazy loading it with dynamic imports. Same result — the crash happens inside componentDidMount, not during import.

I tried downgrading React-Quill versions. None of the published versions support React 19.

The conclusion was clear: React-Quill needed to be replaced entirely.


The Solution: TipTap

TipTap is a modern headless rich text editor built specifically for React. It uses hooks instead of class components, which means it has zero compatibility issues with React 19.

Install it like this:

npm install @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/extension-image @tiptap/extension-link @tiptap/extension-placeholder --legacy-peer-deps

Setting Up TipTap in React 19

Here is a minimal working example:

import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';

const Editor = () => {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Start writing here...</p>',
    editorProps: {
      attributes: {
        class: 'focus:outline-none min-h-64 px-4 py-3',
      },
    },
  });

  // Get HTML content for saving
  const handleSave = () => {
    const html = editor.getHTML();
    console.log(html); // Send this to your API
  };

  return (
    <div>
      <EditorContent editor={editor} />
      <button onClick={handleSave}>Save</button>
    </div>
  );
};

export default Editor;

Two things to note:

  • useEditor() initializes the editor instance

  • EditorContent renders it into the DOM

  • editor.getHTML() gives you the content as an HTML string ready to save to your database


Building a Custom Toolbar

TipTap is headless — it has no built-in toolbar UI. You build your own, which actually gives you full control. Here is how I built mine:

const ToolbarButton = ({ onClick, active, children }) => (
  <button
    type="button"
    onClick={onClick}
    className={`px-2 py-1 rounded text-sm font-medium ${
      active ? 'bg-blue-100 text-blue-700' : 'text-gray-600 hover:bg-gray-100'
    }`}>
    {children}
  </button>
);

// Inside your component:
<div className="flex gap-1 p-2 border-b">
  <ToolbarButton
    onClick={() => editor.chain().focus().toggleBold().run()}
    active={editor.isActive('bold')}>
    <strong>B</strong>
  </ToolbarButton>

  <ToolbarButton
    onClick={() => editor.chain().focus().toggleItalic().run()}
    active={editor.isActive('italic')}>
    <em>I</em>
  </ToolbarButton>

  <ToolbarButton
    onClick={() => editor.chain().focus().toggleBulletList().run()}
    active={editor.isActive('bulletList')}>
    • List
  </ToolbarButton>

  <ToolbarButton
    onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
    active={editor.isActive('heading', { level: 2 })}>
    H2
  </ToolbarButton>
</div>

The chain().focus().toggleBold().run() pattern is TipTap's command API. It chains actions together and executes them on the editor.


One Gotcha: Loading Existing Content

If you are building an edit form where you need to load existing content into the editor, there is a timing issue to be aware of. The editor initializes asynchronously, and your API data also loads asynchronously. If you call editor.commands.setContent() before the editor is ready, nothing happens.

The fix is to use a separate state variable and a useEffect that watches both:

const [postContent, setPostContent] = useState('');

const editor = useEditor({
  extensions: [StarterKit],
  content: '',
});

// Sync content to editor only when BOTH are ready
useEffect(() => {
  if (editor && postContent) {
    editor.commands.setContent(postContent);
  }
}, [editor, postContent]);

// When your API data loads:
const fetchPost = async () => {
  const response = await axios.get(`/api/blog/post/${id}`);
  setPostContent(response.data.content); // This triggers the useEffect
};

This pattern ensures content is set safely regardless of which loads first.


Add List Styles to CSS

TipTap renders lists correctly in the DOM but you need to add CSS to make them visible. Add this to your global stylesheet:

.tiptap ul {
  list-style-type: disc;
  padding-left: 1.5rem;
  margin: 0.5rem 0;
}

.tiptap ol {
  list-style-type: decimal;
  padding-left: 1.5rem;
  margin: 0.5rem 0;
}

.tiptap h2 {
  font-size: 1.5rem;
  font-weight: bold;
  margin: 0.75rem 0;
}

.tiptap blockquote {
  border-left: 4px solid #009ad9;
  padding-left: 1rem;
  color: #6b7280;
}

Without this, bullet points and headings will render invisibly.


Key Takeaway

Before installing any npm package, check whether it supports your React version. React 19 is still new and many popular packages have not updated yet.

If you see findDOMNode is not a function in your console, the package you are using relies on a removed React API. The fix is to find a modern alternative — not to downgrade React.

TipTap is that alternative for rich text editing. It is actively maintained, React 19 ready, and honestly more flexible than React-Quill anyway.


What I'm Building

This editor is part of a 30-day fullstack project I am documenting publicly. I am building a complete digital agency website with React, Node.js, PostgreSQL and Cloudinary. Follow along if you want to see the full build!