react-quill: React 18 New Strict Mode behaviour duplicate toolbar

Hello,

There is a problem with duplicated toolbar using React 18 new strict mode behaviour.

image

New behaviour:

* React mounts the component.
  * Layout effects are created.
  * Effects are created.
* React simulates unmounting the component.
  * Layout effects are destroyed.
  * Effects are destroyed.
* React simulates mounting the component with the previous state.
  * Layout effects are created.
  * Effects are created.

(source)

DEMO: https://codesandbox.io/s/react-quill-react-strict-mode-issue-hlowd0

Ticket due diligence

  • I have verified that the issue persists under ReactQuill v2.0.0-beta.2
  • I can’t use the beta version for other reasons

ReactQuill version

  • master
  • v2.0.0-beta.2
  • v2.0.0-beta.1
  • 1.3.5
  • 1.3.4 or older
  • Other (fork)

FAQ

Is this a bug in Quill or ReactQuill?

ReactQuill is just a ~thin~ wrapper on top of the Quill editor. Often, what looks like a bug in ReactQuill, is actually a bug in the Quill editor itself. Before opening a ticket, please check the Quill documentation, and the issues page, and see if that answers your question first.

How do I access the wrapped Quill instance?

See the instance methods and API documentation.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 22
  • Comments: 29 (4 by maintainers)

Commits related to this issue

Most upvoted comments

As a quick workaround while the PR is not merged, I added this style to my global.css to just hide the extra toolbar

.quill > .ql-toolbar:first-child {
  display: none !important;
}

I see that the PR that fixes this has been merged to master. 👍 When will this get published to npm?

from the current version of react 18 use this syntax to render your whole application :

import React from 'react'
import App from './App'
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css'
import 'react-quill/dist/quill.snow.css';

// this syntax should remove the duplicated toolbar
const container = document.getElementById('root');
const root = createRoot(container); 
root.render(
    <BrowserRouter>
        <App />
    </BrowserRouter>
);

Having the same problem with using React 18 <React.StrictMode>. I think it’s the problem of <React.StrictMode>, when I use only Quill writing, two toolbars also appear. image But when not using strict mode, it doesn’t appear. image In strict mode, this function in useEffect is called twice. image So… maybe it’s a bug of React 18.

Its not bug in react. They changed behaviour how strict mode works, so there is problem with quill. Evidence: https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state .

I am not using react-quill but the quill package directly and I was facing the same issue.

I prevent double rendering the header like this:

...
  const isMounted = useRef(false);
  const editor = useRef(null);

  useEffect(() => {
    if (!isMounted.current) {
      editor.current = new Quill('#editor', {
        theme: 'snow',
      });
      isMounted.current = true;
    }
  }, []);
...

Same probs here… With Next.js, when reactStrictMode on image image while off image image

the logic only works for the bottom toolbar btw

We need to make sure that the toolbar gets created as the child node of our div element. To ensure this we can use the return method of the useEffect hook to clear our div whenever the component unmount. If we do not clear our div, then the toolbar dom gets added with each re-render.

Screenshot 2023-07-01 210847

Hey @Hasan201601

You’re right, some events listeners are detached on destroy and not attached again.

Try following:

class CustomQuill extends ReactQuill {
  destroyEditor() {
    if (!this.editor) return;
    this.unhookEditor(this.editor);
  }

  instantiateEditor() {
    if(this.editor) {
      this.hookEditor(this.editor);
    }
    super.instantiateEditor();
  }
}

I understand, thank you all.

Hello there, I’m facing this issue too with NextJS@12 Any solution !

Having the same problem with using React 18 <React.StrictMode>. I think it’s the problem of <React.StrictMode>, when I use only Quill writing, two toolbars also appear. image But when not using strict mode, it doesn’t appear. image In strict mode, this function in useEffect is called twice. image So… maybe it’s a bug of React 18.

Same problem… I temporarily solved by remove <React.StrictMode> in root.render(). Custom toolbars works even in strict mode.

import Quill from "quill";
import * as React from "react";
import 'quill/dist/quill.snow.css';

export default function App() {
  const ref = React.useRef(null);
  const isMounted = React.useRef(false);

  React.useEffect(() => {
    if (!isMounted.current) {
      ref.current = new Quill(ref.current, {
        theme: 'snow',
      });
      isMounted.current = true;
    }
  }, []);

  return (
      <div ref={ref} />
  );
}

Work For Me 😄

it works for me).

import { useEffect, useRef } from "react";
import Quill from "quill";
import "quill/dist/quill.core.css"; // this line it doesn't work
// ADD import in root css
// @import '~quill/dist/quill.core.css';
// @import '~quill/dist/quill.bubble.css';
// @import '~quill/dist/quill.snow.css';
// OR 
// <!-- Include stylesheet  index.html -->
// <link href="https://cdn.jsdelivr.net/npm/quill@2.0.0/dist/quill.snow.css" rel="stylesheet" />


const toolbarOptions = [
  ["bold", "italic", "underline"], // toggled buttons
  ["blockquote"], //"code-block"
  ["link"],

  [{ list: "ordered" }, { list: "bullet" }, { list: "check" }],
  [{ align: ["", "right", "center", "justify"] }],
  [{ size: ["small", false, "large", "huge"] }], // custom dropdown
  [{ header: [1, 2, 3, 4, 5, 6, false] }],

  [{ color: [] }, { background: [] }], // dropdown with defaults from theme
  [{ font: [] }],
];
// eslint-disable-next-line react/prop-types
const QuillEditor = ({ setText = () => {}, text = "" }) => {
  const editorRef = useRef(null);
  const isMounted = useRef(false);

  useEffect(() => {
    // Fix for renders two toolbar
    if (!isMounted.current) {
      isMounted.current = true;
      return;
    }

    const quill = new Quill(editorRef.current, {
      theme: "snow",
      modules: {
        toolbar: toolbarOptions,
      },
    });
    // Set text from props
    quill.root.innerHTML = text;
    // Detect changes
    quill.on("text-change", () => {
      let content = quill.root.innerHTML;
      setText(content);
    });
    return () => {
      quill.off("text-change");
    };

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Rendering the QuillEditor component with a reference to the DOM element
  return <div ref={editorRef} />;
};

export default QuillEditor;

I initially made use of the Quill editor itself with ReactStrict mode enabled and I faced something similar and with every render a new toolbar was added to the container. However when I turned off ReactStrictmode the issue was resolved but I felt like it was not the proper way to go about building my React application so what I did was look for a solution which appeared to be the Quill editor for React itself so I installed the ReactQuill editor to my application and the issue was resolved you can check it out using the link https://www.npmjs.com/package/react-quill

This fixes the problem. Due to mount - unmount - remount with React Strict Mode. Not sure that my solution is perfect :

const Dossiers = () => {

    useEffect(() => {

        const quill = new Quill('#quill-editor', {theme: 'snow'});

        return () => {

            // get surrounding
            const elem = document.getElementById('quill-editor-surrounding')

            if (!!elem) {
                while (elem.hasChildNodes()) {
                    elem.removeChild(elem.firstChild);
                }

                // create new empty div
                const node = document.createElement("div");

                // change id
                node.id = 'quill-editor'

                // add
                elem.append(node)
            }

        }
    }, [])

    return (
            <div id="quill-editor-surrounding">
                  <div id="quill-editor">
                  </div>
             </div>
    )
}