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

---

## 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:

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

Everything looked fine. Then I opened the page.

---

## The Error

```plaintext
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`:

```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:

```bash
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:

```jsx
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:

```jsx
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:

```jsx
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:

```css
.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!

---
