react-email-editor
react-email-editor copied to clipboard
Renders twice with latest nextjs
The default implementation renders twice using nextjs.
I used this example: https://github.com/unlayer/react-email-editor
and got this outcome

package.json
{
"name": "polyma",
"version": "2.0.0",
"private": true,
"license": "The Unlicense",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"fauna": "fgu"
},
"dependencies": {
"@apollo/client": "^3.5.10",
"@nivo/bar": "^0.79.1",
"@nivo/core": "^0.79.0",
"@nivo/line": "^0.79.1",
"@tailwindcss/line-clamp": "^0.4.0",
"framer-motion": "^6.3.0",
"graphql": "^16.3.0",
"next": "12.1.6",
"react": "^18.0.0",
"react-dnd": "^16.0.0",
"react-dnd-html5-backend": "^16.0.0",
"react-dom": "^18.0.0",
"react-email-editor": "^1.6.0",
"react-hook-form": "^7.30.0",
"react-ranger": "^2.1.0",
"react-use-copy-to-clipboard": "^1.0.1",
"uuid": "^8.3.2"
},
"devDependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-brands-svg-icons": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
"autoprefixer": "^10.4.4",
"eslint": "^8.12.0",
"eslint-config-next": "12.1.6",
"fauna-gql-upload": "^2.4.3",
"faunadb": "^4.5.4",
"postcss": "^8.4.12",
"sharp": "^0.30.3",
"tailwind-scrollbar": "^1.3.1",
"tailwindcss": "^3.0.24",
"vercel": "^24.1.0"
}
}
Full code Email.js
import { useRef } from "react";
import dynamic from "next/dynamic";
const EmailEditor = dynamic(() => import("react-email-editor"), {
ssr: false,
});
export default function Email() {
const emailEditorRef = useRef(null);
function saveDesign() {
emailEditorRef.current.editor.saveDesign((design) => {
console.log("saveDesign", design);
});
}
function exportHtml() {
emailEditorRef.current.editor.exportHtml((data) => {
const { design, html } = data;
console.log("exportHtml", html);
});
}
function onLoad() {
// editor instance is created
// you can load your template here;
// const templateJson = {};
// emailEditorRef.current.editor.loadDesign(templateJson);
}
function onReady() {
// editor is ready
console.log("onReady");
}
return (
<div className="">
<div>
<button onClick={saveDesign}>Save Design</button>
<button onClick={exportHtml}>Export HTML</button>
</div>
<div className="h-screen overflow-hidden">
<EmailEditor
ref={emailEditorRef}
onLoad={onLoad}
onReady={onReady}
// works for now, the second email builder gets hidden.
// minHeight="100vh"
/>
</div>
</div>
);
}
The iframe is injected twice.

Hello, I was having the same problem in React V18. And I saw that the problem was caused by the fact that Strict Mode causes the element to be rendered twice in React. I solved the problem by removing the StricMode tag in index.js.
root.render(
<> // Without StrictMode
<App />
</>
);
Solved it for me. However, that means there is some legacy code running. I'll leave it open for now. :)
Having the same issue today, I really feel like turning off StrictMode is not a good workaround though.
For anyone that does not mind this, turning it off does indeed fix the issue.
In case anyone is wondering why turning of StrictMode fixes the issue
https://stackoverflow.com/a/61897567/12035491
My dirty solution
const removeUnusedIframes = () => {
const frames = document.getElementsByTagName('iframe');
const unlayerFrames = Array.from(frames).filter((frame) => frame.src.includes('unlayer'));
if (unlayerFrames.length === 1) return;
unlayerFrames.slice(1).forEach((frame) => frame.remove());
};
This should be resolved with the merge. Thanks @otaviosoares
Hey guys, I'm running into the same issue here:
import React, { useRef, useState, useEffect } from "react";
import dynamic from "next/dynamic";
import type { NextPage } from "next";
// Assuming the EmailEditorProps and EditorRef are correctly exported from react-email-editor
import { EditorRef } from "react-email-editor";
import templateJson from "./utils/emailTemplate.json";
const EmailEditor = dynamic(
() => import("react-email-editor").then((mod) => mod.EmailEditor),
{ ssr: false }
);
const TemplateEditor: NextPage = (props: any) => {
const emailEditorRef = useRef<EditorRef>(null);
const [isReady, setIsReady] = useState(false);
const initialized = useRef(false);
const exportHtml = () => {
console.log("exportHtml");
const unlayer = emailEditorRef.current?.editor;
unlayer?.exportHtml((data) => {
const { design, html } = data;
console.log("exportHtml", html);
});
};
const onReady = (unlayer: any) => {
console.log("onReady");
if (!initialized.current) {
unlayer.loadDesign(templateJson);
setIsReady(true);
initialized.current = true;
}
};
const onLoad = (unlayer: any) => {
console.log("onLoad");
unlayer.init({
id: "editor-container",
displayMode: "email",
customJS: [
"https://examples.unlayer.com/examples/simple-custom-tool/custom.js",
],
});
};
useEffect(() => {
const handleReady = (unlayer: any) => {
console.log("handleReady");
// Ensure the editor is ready before loading the design
unlayer.loadDesign(templateJson);
};
const addEventListener = () => {
console.log("addEventListener");
if (emailEditorRef.current) {
emailEditorRef.current.editor?.addEventListener(
"ready",
handleReady
);
}
};
const removeEventListener = () => {
if (emailEditorRef.current) {
emailEditorRef.current.editor?.removeEventListener("ready");
}
};
addEventListener();
// Return the cleanup function to remove the event listener
return removeEventListener;
}, [emailEditorRef.current]);
return (
<div id="editor-container" className="flex h-screen">
{isReady && (
<div>
<button onClick={exportHtml}>Export HTML</button>
</div>
)}{" "}
<EmailEditor
editorId="email-editor"
ref={emailEditorRef}
onLoad={onLoad}
onReady={onReady}
style={{ height: "100%" }}
/>
</div>
);
};
export default TemplateEditor;