react-quill icon indicating copy to clipboard operation
react-quill copied to clipboard

useRef to manipulate ReactQuill in a custom handles breaks rendering

Open alefduarte opened this issue 5 years ago • 11 comments
trafficstars

When using functional component with react hook, the useRef hook breaks the rendering when changing state. I have noticed that by adding a ref to ReactQuill, in order to access it through a custom handles, crashes the rendering. Without a ref, the component works fine but I can't manipulate it through a handler, also, without the value={value} property, it works fine, but I can't manage the value state.

I've made a pen as example, in my computer, both in the project I've been working and in an isolated project, when typing in the editor, it goes blank, it's just dropped from DOM. When using in codepen, it behaves oddly when typing and loses focus each change.

Codepen

  • [X] 2.0.0-beta-1

alefduarte avatar Apr 17 '20 00:04 alefduarte

This is fixed in v2.0.0-beta.2! You can quickly verify the fix by updating your Codepen to use https://unpkg.com/react-quill@beta/dist/react-quill.js. Can you confirm this solves your issues in your main project as well?

zenoamaro avatar Apr 18 '20 19:04 zenoamaro

It worked! But I came across another error :/ when I trigger the custom handler, I get a "Maximum update depth exceeded" error. It happens when I choose an style "uppercase, lowercase, capitalize" from the custom "Style" dropdown. It then sets it in like so: node.setAttribute('style', `text-transform: ${value};`);, with value being the three styles mentioned above.

I noticed removing the section static formats(domNode) { return domNode.getAttribute('style') || true; } solves the problem, but then the style is not set, so I suppose the error is triggered in this section.

You can check it in the same codepen. Select a part of the text and choose a style. Screenshot from 2020-04-18 17-05-50

The same error happened in my main project, but the first one is fixed. I also noticed a long delay after each key is pressed and it warns "addRange(): The given range isn't in document."

alefduarte avatar Apr 18 '20 20:04 alefduarte

In v1.3.5, can't support custom upload image, when I update to v2.0.0-beta.2, It works, But now can't input chinese when I config image handler, It's ok close it.


  const imageHandle = () => {
    const editor = quillRef.current.getEditor()
    const input = document.createElement('input')
    input.setAttribute('type', 'file')
    input.setAttribute('accept', 'image/*')
    // input.setAttribute('multiple', 'multiple')
    input.click()
    input.onchange = async () => {
      const files = input.files
      try {
        const reqData = await getData()
        const list = await Promise.all(
          Array.from(files, file =>
            upload({
              file,
              action: '/upload/image',
              filename: 'image',
              data: {
                token: generateToken(),
                ...reqData
              },
              headers: {}
            })
          )
        )
        console.log('list:', list)
        console.log('getSelection:', editor.getSelection, editor.getSelection())
        console.log('getLength:', editor.getLength, editor.getLength())
        console.log('editor:', editor)
        list.forEach(url => editor.insertEmbed(editor.getSelection(), 'image', url))
      } catch (err) {
        message.error('upload err, try again!')
      }
    }
  }
  const initModules = {
    toolbar: {
      container: toolbarOptions
      // handlers: {
      //   image: imageHandle
      // }
    },
    clipboard: {
      // toggle to add extra line breaks when pasting HTML:
      matchVisual: false
    }
  }

OnlyFlyer avatar May 19 '20 09:05 OnlyFlyer

In v1.3.5, can't support custom upload image, when I update to v2.0.0-beta.2, It works, But now can't input chinese when I config image handler, It's ok close it.

  const imageHandle = () => {
    const editor = quillRef.current.getEditor()
    const input = document.createElement('input')
    input.setAttribute('type', 'file')
    input.setAttribute('accept', 'image/*')
    // input.setAttribute('multiple', 'multiple')
    input.click()
    input.onchange = async () => {
      const files = input.files
      try {
        const reqData = await getData()
        const list = await Promise.all(
          Array.from(files, file =>
            upload({
              file,
              action: '/upload/image',
              filename: 'image',
              data: {
                token: generateToken(),
                ...reqData
              },
              headers: {}
            })
          )
        )
        console.log('list:', list)
        console.log('getSelection:', editor.getSelection, editor.getSelection())
        console.log('getLength:', editor.getLength, editor.getLength())
        console.log('editor:', editor)
        list.forEach(url => editor.insertEmbed(editor.getSelection(), 'image', url))
      } catch (err) {
        message.error('upload err, try again!')
      }
    }
  }
  const initModules = {
    toolbar: {
      container: toolbarOptions
      // handlers: {
      //   image: imageHandle
      // }
    },
    clipboard: {
      // toggle to add extra line breaks when pasting HTML:
      matchVisual: false
    }
  }

use useCallback

leesama avatar Jul 29 '20 09:07 leesama

@leesama Can you elaborate? I'm facing the same issue.

thealpha93 avatar Sep 11 '20 17:09 thealpha93

You need to use useMemo or useCallback to memoize the plugin function and object creations, if these plugins are defined inline in a function component. The ReactQuill wrapper will rebuild the editor if these values change identity.

alexkrolick avatar Sep 11 '20 20:09 alexkrolick

You need to use useMemo or useCallback to memoize the plugin function and object creations, if these plugins are defined inline in a function component. The ReactQuill wrapper will rebuild the editor if these values change identity.

I was having an issue, with a custom image handler, where any local state changes would make the quill disappear. Wrapping my custom handler in a useCallback worked for me.

GavinThomas1192 avatar Oct 12 '20 17:10 GavinThomas1192

@alexkrolick @leesama @zenoamaro

Below is the shortened version of my code implementation. ImageHandler is a custom Function. I am not able to quillRef.current.getEditor().

Any help on how to get the editor, is highly appreciated , i am using NextJs if thats helpful.

const imageHandler = () => {
    const editor = quillRef.current.getEditor(); // The error here TypeError: quillRef.current.getEditor is not a function
const input = document.createElement("input");

    console.log(edit);
    input.setAttribute("type", "file");
    input.click();

    // Listen upload local image and save to server
    input.onchange = () => {
      const file = input.files[0];

      // file type is only image.
      if (/^image\//.test(file.type)) {
        saveToServer(file);
      } else {
        console.warn("You could only upload images.");
      }
    };
  };

/**
   * Step2. save to server
   *
   * @param {File} file
   */
  function saveToServer(file) {
    var formData = new FormData();
    formData.append("file", file);
    axios
      .post(`${process.env.NEXT_PUBLIC_API}/upload-file`, formData, {
        headers: {
          "Content-Type": "multipart/form-data",
        },
        responseType: "json",
      })
      .then((res) => {
        if (res.data.ok == true) {
          insertToEditor(res.data.fileUrl);
        } else {
          console.error(res.data);
        }
      })
      .catch((e) => {
        console.error(e);
      });
  }

  /**
   * Step3. insert image url to rich editor.
   *
   * @param {string} url
   */
  function insertToEditor(url) {
    // push image url to rich editor.
    const range = quillRef.current.getEditor().getSelection(); // This also fails , the idea is to insert the uploaded image to the selected position
    range.insertEmbed(range.index, "image", `http://localhost:9000${url}`);
  }

const modules = {
    toolbar: {
      container: [
        [{ header: [1, 2, false] }],
        ["bold", "italic", "underline", "strike", "blockquote"],
        [
          { list: "ordered" },
          { list: "bullet" },
          { indent: "-1" },
          { indent: "+1" },
        ],
        ["link", "image"],
        [{ align: [] }],
        ["clean"],
      ],
      handlers: {
        image: imageHandler,
      },
    },
  };

<ReactQuill
    ref={quillRef} //UseRef
    modules={modules}
    theme="snow"
    value={quill}
    onChange={handleReactQuill}
  />

TheDarkStrix avatar Jun 05 '21 10:06 TheDarkStrix

@TheDarkStrix wrap the modules inside useMemo()

const modules=useMemo(()=>({
  toolbar: {
    container: [
      [{ header: [1, 2, false] }],
      ["bold", "italic", "underline", "strike", "blockquote"],
      [
        { list: "ordered" },
        { list: "bullet" },
        { indent: "-1" },
        { indent: "+1" },
      ],
      ["link", "image"],
      [{ align: [] }],
      ["clean"],
    ],
    handlers: {
      image: imageHandler,
    },
  },
}),[])

singh-inder avatar Mar 05 '22 13:03 singh-inder

I already wrapped my modules in useMemo(), still these: const editor = quillRef.current.getEditor(); // The error here TypeError: quillRef.current.getEditor is not a function const range = quillRef.current.getEditor().getSelection(); // This also fails

is not working.

Please help.

brodwen83 avatar Aug 07 '22 13:08 brodwen83

I have found a solution. they used a forwardRef. Dynamic import doesn't support ref.

Solution: https://github.com/zenoamaro/react-quill/issues/642#issuecomment-717661518

const QuillNoSSRWrapper = dynamic( async () => { const { default: RQ } = await import('react-quill'); // eslint-disable-next-line react/display-name return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />; }, { ssr: false } );

then <QuillNoSSRWrapper forwardedRef={quillRef} ... />

brodwen83 avatar Aug 07 '22 14:08 brodwen83

I have found a solution. they used a forwardRef. Dynamic import doesn't support ref.

Solution: #642 (comment)

const QuillNoSSRWrapper = dynamic( async () => { const { default: RQ } = await import('react-quill'); // eslint-disable-next-line react/display-name return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />; }, { ssr: false } );

then <QuillNoSSRWrapper forwardedRef={quillRef} ... />

where should i place this , in other words do you know how to upload images to cloudinary

codecret avatar Oct 26 '22 08:10 codecret

You can use it without any problem. Bug free version

import dynamic from 'next/dynamic';
import React, { useState, useRef, useEffect, useMemo } from 'react';

const ReactQuill = dynamic(
  async () => {
    const { default: RQ } = await import('react-quill');

    // eslint-disable-next-line react/display-name
    return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;
  },
  {
    ssr: false,
  }
);

export default function IndexPage() {
  const [value, setValue] = useState('');
  const quillRef = useRef();

  useEffect(() => {
    const init = (quill) => {};
    const check = () => {
      if (quillRef.current) {
        init(quillRef.current);
        return;
      }
      setTimeout(check, 200);
    };
    check();
  }, [quillRef]);

  const imageHandler = () => {
    const input = document.createElement('input');

    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');
    input.click();

    input.onchange = async () => {
      const file = input.files[0];
      const formData = new FormData();

      formData.append('image', file);

      // Save current cursor state
      const range = quillRef.current.editor.getSelection(true);

      // Insert temporary loading placeholder image
      quillRef.current.editor.insertEmbed(range.index, 'image', `${window.location.origin}/images/loaders/placeholder.gif`);

      // Move cursor to right side of image (easier to continue typing)
      quillRef.current.editor.setSelection(range.index + 1);

      const res = 'https://res.cloudinary.com/db6kegyyc/image/upload/v1670236604/o8xdwlwarz1ejhxvpchw.png';

      // Remove placeholder image
      quillRef.current.editor.deleteText(range.index, 1);

      // Insert uploaded image
      // this.quill.insertEmbed(range.index, 'image', res.body.image);
      quillRef.current.editor.insertEmbed(range.index, 'image', res);
    };
  };

  const modules = useMemo(
    () => ({
      toolbar: {
        container: [
          [{ header: [1, 2, false] }],
          ['bold', 'italic', 'underline', 'strike', 'blockquote'],
          [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
          ['link', 'image'],
          [{ align: [] }],
          ['clean'],
        ],
        handlers: {
          image: imageHandler,
        },
      },
    }),
    []
  );

  return <ReactQuill forwardedRef={quillRef} modules={modules}  value={value} onChange={setValue} />;
}


bayoguzhan95 avatar Dec 17 '22 23:12 bayoguzhan95