I’m encountering an error while integrating the quill-mention module with react-quill in a Next.js application. The error message I see is:
Unhandled Runtime Error
TypeError: moduleClass is not a constructor
Call Stack
SnowTheme.addModule
(node_modules/react-quill/node_modules/quill/dist/quill.js (6130:0))
SnowTheme.addModule
(node_modules/react-quill/node_modules/quill/dist/quill.js (6774:0))
eval
(node_modules/react-quill/node_modules/quill/dist/quill.js (6122:0))
Array.forEach
<anonymous>
SnowTheme.init
Here is how I’m trying to use the quill-mention module:
"use client";
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
import dynamic from "next/dynamic";
import Quill from "quill";
import Mention from "quill-mention";
import "react-quill/dist/quill.snow.css";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import TooltipCommon from "@/components/common/TooltipCommon";
import { useCopywriterStore } from "@/Store/CopywriterStore";
import { useUserStore } from "@/Store/UserStore";
import { useEditorStore } from "@/Store/EditorStore";
// Dynamically import ReactQuill to prevent SSR issues
const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
interface QuillEditorProps {
customerId: string | string[];
updateId: string | string[];
indicatorText: string | string[];
handleEdit: string | string[];
orderId: string | string[];
productFlowId: string | string[];
leadId: string | string[];
technicalId: string | string[];
copywriterId: string | string[];
amendmentId: string | string[];
websiteContentId: string | string[];
setIsOpenReplyModel: Dispatch<SetStateAction<boolean>>;
setOpenQuill: Dispatch<SetStateAction<boolean>>;
}
const QuillEditor: React.FC<QuillEditorProps> = ({
customerId,
setIsOpenReplyModel,
setOpenQuill,
updateId,
productFlowId,
orderId,
technicalId,
leadId,
indicatorText,
amendmentId,
copywriterId,
websiteContentId,
handleEdit,
handlesave,
editContent,
}: any) => {
const [value, setValue] = useState<string>("");
const [images, setImages] = useState<File[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [fileURLs, setFileURLs] = useState<{ [key: string]: string }>({});
const { fetchCopywriterData }: any = useCopywriterStore();
const { fetchUsersData, userData } = useUserStore();
const {
fetchEditorData,
fetchLeadsEditorData,
fetchOrderEditorData,
fetchTechnicalUpdateData,
fetchAmendmentUpdateData,
fetchProductFlowUpdateData,
fetchWebsiteContentUpdateData,
orderEditorData,
fetchCopywriterUpdateData,
editorData,
addReplyData,
addUpdateData,
loading,
}: any = useEditorStore();
const handleClear = () => {
setValue("");
};
const handleChanges = (value: string, editorData: any) => {
setValue(() =>
editorData.getText().trim() === "" && value === "" ? "" : value
);
};
const handleAddData = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if ((value !== "" && value !== "<p><br></p>") || images.length > 0) {
try {
setIsLoading(true);
const formData = new FormData();
formData.append("content", value);
images.forEach((image) => formData.append("files", image));
const requests = [];
if (indicatorText === "post") {
requests.push(
orderId && baseInstance.post(`/updates/order/${orderId}`, formData),
productFlowId &&
baseInstance.post(
`/updates/productflow/${productFlowId}`,
formData
),
customerId &&
baseInstance.post(`/updates/customer/${customerId}`, formData),
leadId && baseInstance.post(`/updates/lead/${leadId}`, formData),
technicalId &&
baseInstance.post(
`/updates/technicaltracker/${technicalId}`,
formData
),
copywriterId &&
baseInstance.post(
`/updates/copywritertracker/${copywriterId}`,
formData
),
websiteContentId &&
baseInstance.post(
`/updates/newwebsitecontent/${websiteContentId}`,
formData
),
amendmentId &&
baseInstance.post(`/updates/amendment/${amendmentId}`, formData)
);
}
if (indicatorText === "reply") {
requests.push(
baseInstance.post(`/updates/update/reply/${updateId}`, formData)
);
}
const responses = await Promise.all(requests.filter(Boolean));
responses.forEach((response) => {
if (response.status === 201) {
successToastingFunction(response?.data?.message);
fetchEditorData(customerId);
fetchOrderEditorData(orderId);
fetchLeadsEditorData(leadId);
fetchTechnicalUpdateData(technicalId);
fetchCopywriterUpdateData(copywriterId);
fetchAmendmentUpdateData(amendmentId);
fetchProductFlowUpdateData(productFlowId);
fetchWebsiteContentUpdateData(websiteContentId);
setIsOpenReplyModel(false);
setOpenQuill(false);
handleClear();
setImages([]);
setValue("");
}
fetchEditorData(customerId);
});
} catch (error) {
errorToastingFunction(error);
} finally {
setIsLoading(false);
}
} else {
errorToastingFunction("Please enter text or upload an image to submit.");
}
};
const handleFileUpload = (files: FileList | null) => {
if (files) {
const fileList = Array.from(files);
setImages((prevImages) => [...prevImages, ...fileList]);
fileList.forEach((file) => {
if (file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onloadend = () => {
const img = `<img src="${reader.result}" alt="${file.name}" />`;
setValue((prevValue) => prevValue + img + `</br>`);
};
reader.readAsDataURL(file);
} else {
const url = URL.createObjectURL(file);
setFileURLs((prev) => ({ ...prev, [file.name]: url }));
const link = `<a href="${url}" target="_blank" rel="noopener noreferrer">${file.name}</a>`;
setValue((prevValue) => prevValue + link + `</br>`);
}
});
}
};
const imageHandler = () => {
const inputImage = document.createElement("input");
inputImage.setAttribute("type", "file");
inputImage.setAttribute(
"accept",
"image/*, video/*, .pdf, .xlsx, .doc, .docx"
);
inputImage.setAttribute("multiple", "true");
inputImage.click();
inputImage.onchange = (e) => {
handleFileUpload(inputImage.files);
};
};
useEffect(() => {
return () => {
Object.values(fileURLs).forEach((url) => URL.revokeObjectURL(url));
};
}, [fileURLs]);
// Commented out because it causes an error
// useEffect(() => {
// if (Quill) {
// Quill.register("modules/imageResize", ImageResize);
// Quill.register("modules/mention", Mention);
// }
// }, [Quill]);
const toolbarOptions = [
["bold", "italic", "underline", "strike"],
[{ size: ["small", false, "large", "huge"] }],
[{ list: "ordered" }, { list: "bullet" }, { list: "check" }],
[{ script: "sub" }, { script: "super" }],
[{ indent: "-1" }, { indent: "+1" }],
[{ direction: "rtl" }],
[{ color: [] }, { background: [] }],
[{ font: [] }],
[{ align: [] }],
];
const options = {
debug: "info",
modules: {
toolbar: toolbarOptions,
imageResize: {
parchment: Quill.import("parchment"),
modules: ["Resize", "DisplaySize"],
},
mention: {
allowedChars: /^[A-Za-zs]*$/,
mentionDenotationChars: ["@"],
source: async (
searchTerm: string,
renderItem: (data: any[]) => void
) => {
try {
await fetchUsersData(); // Ensure data is fetched first
const filteredUsers = userData
.filter((user: any) =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
)
.map((user: any) => ({ value: user.name }));
renderItem(filteredUsers);
} catch (error) {
console.error("Error fetching mention users:", error);
renderItem([]);
}
},
},
},
placeholder: "Compose an epic...",
theme: "snow",
};
return (
<>
<form onSubmit={handleAddData} className="flex gap-2 flex-col ">
<div className="">
<ReactQuill
placeholder={options.placeholder}
theme={options.theme}
modules={options.modules}
value={value}
onChange={(value, _, __, editor) => {
handleChanges(value, editor);
}}
/>
</div>
<div className="flex justify-start gap-2 items-center">
<Button
type="submit"
className="cursor-pointer h-[24px] rounded-lg border border-primary bg-primary px-4 text-white transition hover:bg-opacity-90"
>
{isLoading ? (
<Loader2 className="mr-2 h-6 w-6 animate-spin text-[#fff]" />
) : indicatorText === "reply" ? (
"Reply"
) : (
"Update"
)}
</Button>
<div onClick={imageHandler} className="w-fit cursor-pointer ">
<TooltipCommon text="Add Files">
<div className="hover:bg-gray-100 px-2 py-1 rounded">
<AddFilesDarkUIconSVG />
</div>
</TooltipCommon>
</div>
</div>
</form>
</>
);
};
export default QuillEditor;
I’ve integrated the quill-mention module into my react-quill component but am running into an issue. The error TypeError: moduleClass is not a constructor appears, and the call stack indicates a problem with SnowTheme.addModule in the Quill.js file.
I’ve tried to register the module as follows:
useEffect(() => {
if (Quill) {
Quill.register("modules/imageResize", ImageResize);
Quill.register("modules/mention", Mention);
}
}, [Quill]);
However, this causes the mentioned error. I’ve also commented out the registration as it seems to be the cause of the issue.
What I’ve Tried:
Ensuring that the quill-mention module is correctly imported and used.
Checking the version compatibility of react-quill, quill, and quill-mention.
Referencing the Quill documentation for proper module registration.
What I Need Help With:
Why am I encountering this TypeError?
How can I properly integrate the quill-mention module with react-quill?
Are there any additional steps or configurations required to avoid this error?
Any guidance or suggestions would be greatly appreciated!