react-quill
react-quill copied to clipboard
useRef to manipulate ReactQuill in a custom handles breaks rendering
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.
- [X] 2.0.0-beta-1
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?
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.

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."
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
}
}
In
v1.3.5, can't support custom upload image, when I update tov2.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 Can you elaborate? I'm facing the same issue.
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.
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.
@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 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,
},
},
}),[])
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.
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} ... />
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
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} />;
}