I am implementing a React component using the Quill library.
I’m having a problem inserting an image into the text editor. When I select the image, the editor crashes and the whole page freezes. It is only after many seconds that the image is inserted into the editor. However, after that I still can’t do anything, the crash persists. I have to refresh the page…
Here is what I currently have:
// EditorField.tsx
[...] // All imports.
const IMAGE_ACCEPT = '.png, .jpeg, .jpg';
const IMAGE_MAX_SIZE = SIZES_BYTES.Mo;
/**
* EditorField component.
*
* @param {EditorFieldProps} props Props
* @return {JSX.Element}
*/
const EditorField = forwardRef(
(
{
className,
errorMessage,
helperText,
label,
onChange,
placeholder,
readOnly,
required,
toolbar = EDITORFIELD_TOOLBAR.DEFAULT,
value,
...attr
}: EditorFieldProps,
ref?: ForwardedRef<HTMLDivElement>,
) => {
const containerRef = useForwardedRef<HTMLDivElement>(ref);
const quillRef = useRef<Quill | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [editorErrorMessage, setEditorErrorMessage] = useState<string>('');
const classNames = clsx('OmniEditorField', className);
/**
* Function called when the text changes.
*
* @return {void}
*/
const onTextChange = useCallback((): void => {
const html =
containerRef.current?.querySelector('.ql-editor')?.innerHTML || '';
onChange(html === '<p><br></p>' ? '' : html);
}, [containerRef, onChange]);
/**
* Add image in editor.
*
* @param {ChangeEvent<HTMLInputElement>} event Event
* @return {Promise<void>}
*/
const handleAddImage = async (
event: ChangeEvent<HTMLInputElement>,
): Promise<void> => {
const file = event.target.files?.[0];
const currentSelection = quillRef.current?.getSelection();
if (!file || !quillRef.current || !currentSelection) return;
// Removes a previous error that may have occurred.
setEditorErrorMessage('');
// Check image size.
if (file.size > IMAGE_MAX_SIZE) {
setEditorErrorMessage(
`La taille de l'image ne doit pas dépasser la limite de ${formatBytes(IMAGE_MAX_SIZE)}`,
);
return;
}
// Check image type.
if (!checkFileType(file, IMAGE_ACCEPT)) {
setEditorErrorMessage("Le type de l'image n'est pas autorisé.");
return;
}
// Insert image.
await convertFileToBase64(file)
.then((file64) => {
quillRef.current?.editor.insertEmbed(
currentSelection.index,
'image',
file64,
);
quillRef.current?.insertText(currentSelection.index + 1, 'n');
quillRef.current?.setSelection(currentSelection.index + 2);
})
.catch(() => {
setEditorErrorMessage(
"Une erreur s'est produite lors de l'insertion de l'image.",
);
});
};
useEffect(() => {
const container = containerRef.current;
if (!container) return;
// Init quill.
const quill = new Quill(
container.appendChild(container.ownerDocument.createElement('div')),
{
modules: {
toolbar: {
container: toolbar,
handlers: {
image: (imageValue: boolean) => {
if (imageValue) {
inputRef.current?.click();
}
},
},
},
},
placeholder,
theme: 'snow',
},
);
quillRef.current = quill;
// eslint-disable-next-line consistent-return
return () => {
container.innerHTML = '';
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Set events.
useEffect(() => {
quillRef.current?.on(Quill.events.TEXT_CHANGE, onTextChange);
return () => {
quillRef.current?.off(Quill.events.TEXT_CHANGE, onTextChange);
};
}, [onTextChange]);
// Set read only.
useEffect(() => {
quillRef.current?.enable(!readOnly);
}, [quillRef, readOnly]);
// Set value.
useEffect(() => {
if (!quillRef.current || quillRef.current.hasFocus()) return;
const currentSelection = quillRef.current.getSelection();
quillRef.current.setContents(
quillRef.current.clipboard.convert({ html: value }),
);
quillRef.current.setSelection(currentSelection);
}, [value]);
return (
<div className={classNames} data-testid="OmniEditorField">
<FormLabel as="span" required={required}>
{label}
</FormLabel>
<FormHelperText>{helperText}</FormHelperText>
<input
accept={IMAGE_ACCEPT}
className="OmniEditorField__file"
hidden
onChange={handleAddImage}
ref={inputRef}
type="file"
/>
<div className="OmniEditorField__field" ref={containerRef} {...attr} />
<FormErrorMessage>
{editorErrorMessage || errorMessage}
</FormErrorMessage>
</div>
);
},
);
export { EditorField };
// EditorField.stories.tsx
[...] // All imports.
// Types.
type Meta = MetaStorybook<EditorFieldProps>;
type Story = StoryObj<EditorFieldProps>;
// Metadata.
const meta: Meta = {
component: EditorField,
tags: ['autodocs'],
title: 'Composants/Formulaire/EditorField',
};
export default meta;
// Stories.
export const EditorFieldStory: Story = {
name: 'Champ de base',
args: {
helperText: 'Parlez nous un peu de vous !',
label: 'Biographie',
placeholder: 'Saisissez votre biographie...',
required: true,
value: "Je m'appelle John Doe...",
},
render: ({ value: defaultValue, ...args }) => {
const [value, setValue] = useState<string>(defaultValue);
return <EditorField {...args} value={value} onChange={setValue} />;
},
};
Some project package versions:
"quill": "2.0.2",
"react": "18.3.1",
"react-dom": "18.3.1",
"vite": "5.2.12",
"typescript": "5.2.2",
"storybook": "8.1.5",
If the field is not controlled, everything works without problems but as I want to make a custom controlled component with the value
and onChange
properties, I have problems.
I tried, as in the quill example, to use only “ref” but without success.
I tried to understand the problem, the editor does not crash if I replace the value like this :
/**
* Add image in editor.
*
* @param {ChangeEvent<HTMLInputElement>} event Event
* @return {Promise<void>}
*/
const handleAddImage = async (
event: ChangeEvent<HTMLInputElement>,
): Promise<void> => {
[...]
// Insert image.
await convertFileToBase64(file)
.then((file64) => {
quillRef.current?.editor.insertEmbed(
currentSelection.index,
'image',
'', // Replace file64 into empty string.
);
quillRef.current?.insertText(currentSelection.index + 1, 'n');
quillRef.current?.setSelection(currentSelection.index + 2);
})
.catch(() => {
setEditorErrorMessage(
"Une erreur s'est produite lors de l'insertion de l'image.",
);
});
If I hide the onChange
function, again the editor does not crash:
/**
* Function called when the text changes.
*
* @return {void}
*/
const onTextChange = useCallback((): void => {
const html =
containerRef.current?.querySelector('.ql-editor')?.innerHTML || '';
// onChange(html === '<p><br></p>' ? '' : html);
}, [containerRef, onChange]);
Quentin Masbernat is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.