[Bug] Double codex editor in Reactjs
I'am trying to setup editor.js in my React app. But there is a weird situation in my project. Let me demonstrate:

There is second codex-editor which is I don't need it. Also the second codex-editor is also main(?) editor. When I console output, it shows the second editor's values:
My React app code:
import EditorJS from "@editorjs/editorjs";
import Header from "@editorjs/header";
import List from "@editorjs/list";
import "./App.scss";
function App() {
const editor = new EditorJS({
/**
* Id of Element that should contain the Editor
*/
holder: "editorjs",
/**
* Available Tools list.
* Pass Tool's class or Settings object for each Tool you want to use
*/
tools: {
header: {
class: Header,
inlineToolbar: ["link"],
},
list: {
class: List,
inlineToolbar: true,
},
},
});
const onClickHandler = () => {
editor
.save()
.then((outputData) => {
console.log("Article data: ", outputData.blocks);
})
.catch((error) => {
console.log("Saving failed: ", error);
});
};
return (
<div className="App">
<div id="editorjs"></div>
<button onClick={onClickHandler}>Add</button>
</div>
);
}
export default App;
Hi 👋 Try to init editor in useEffect hook. Like this:
useEffect(() => {
new EditorJS(...);
}, []);
@ilyamore88 this time my onClickHandler will not work. it stores the output data from editor
It isn't a bug in EditorJS. You need to init an instance correct. If you use useEffect hook with empty deps, it renders editor only on componentDidMount hook. Also, you need to think about destructor in this hook.
You can try to use ref for this.
@ilyamore88 , I should have downloaded react-editor-js as well. My jsx is kinda so:
import React, { useEffect, useReducer, useState, useRef } from "react";
import "./App.css";
import EditorJs from "react-editor-js";
import { createReactEditorJS } from "react-editor-js";
import { EDITOR_JS_TOOLS } from "./constants";
// import DataFetching from "./components/DataFetching";
function App() {
const instanceRef = useRef(null);
const ReactEditorJS = createReactEditorJS();
console.log(instanceRef);
async function handleSave() {
const savedData = await instanceRef.current.save();
console.log("savedData", savedData);
}
return (
<React.Fragment>
<button onClick={handleSave}>Save!</button>
<ReactEditorJS
onChange={handleSave}
instanceRef={(instance) => (instanceRef.current = instance)}
tools={EDITOR_JS_TOOLS}
/>
</React.Fragment>
);
}
export default App;
But I have an error on console. my console error:
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'save')
It's weird, cuz I do not know how to prevent that null error. I've been working this issue for an hour, but still I can't figure out a solution.
@azadsarxanli please try with : ` const editorJS = React.useRef(null) const handleInitialize = React.useCallback((instance) => { editorJS.current = instance }, [])
const handleSave = React.useCallback( async () => {
const savedData = await editorJS.current.save();
console.log(savedData);
}, [])
and<ReactEditorJS
onInitialize={handleInitialize}
tools={EDITOR_JS_TOOLS}...`
Use a ref and only initialise if it's null and destroy on unmount.
const editorInstance = useRef();
useEffect(() => {
if (!editorInstance.current) {
initEditor();
}
return () => {
editorInstance.current.destroy();
editorInstance.current = null;
}
}, []);
const initEditor = () => {
const editor = new EditorJS({
...props,
onReady: (api) => {
editorInstance.current = editor;
},
onChange: async (api, event) => {
handleSave(...);
},
});
}
I'm using React 18 (with strict mode) and I'm trying to use useEffect to init EditorJs.
useEffect(() => {
const editor = new EditorJS({
placeholder: "Write ",
holder: "editorjs",
tools,
});
return () => {
editor.isReady.then(() => {
editor.destroy();
});
};
}, []);
The problem is that editor.destroy(); seems to destroy all instances, so I don't see any editors in my component.
https://github.com/codex-team/editor.js/blob/f7368edee3fd5af32a6a9e9903bb9a00a94cff68/src/codex.ts#L84-L102
Is there a way to destroy only 1 instance?
Only using useEffect doesn't prevent from rendering twice. Using state to check editorjs instance exist was working for me though I'm not sure this is correct way. d
const id = useId();
const [editor, setEditor] = useState<EditorJS | null>(null);
useEffect(() => {
setEditor((prevEditor) => {
if (!prevEditor) {
return new EditorJS({
holder: id,
});
}
return null;
});
return () => {
if (editor) {
editor.destroy();
}
};
}, []);
return <div id={id} />;
Hi, It is because of rendering component twice, to solve this problem, when you initilize the editor in useEffect, use empty braces [] as useEffect's dependency, so anything in useEffect will render once per each component call. And in the useEffect, return a cleanup function which destroy the editor when component detaches from the dom.
useEffect(() => {
const editor = new EditorJs();
return () => {
editor.destroy();
}
}, []);
Hi, I face the same issue. But I have found this answer from stackoverflow here.
I tried to build my app and there is no issue.
For development, I comment ReactStrictMode like this :
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
// <React.StrictMode>
<QueryClientProvider client={queryClient}>
<ConfigProvider theme={CustomTheme}>
<RecoilRoot>
<RouterProvider router={routes} />
</RecoilRoot>
</ConfigProvider>
</QueryClientProvider>
// </React.StrictMode>
);
Also find the reason why it's rendered twice on Strict Mode from geeksforgeeks.org :
Hope it helps!
I use useEffect to init editor and return a destory callback to React to destory Editorjs instance before component unmount.
On the other hand, I use a state to indicate whether the rendering function has been executed. If it has been executed, it prevents the rendering function from being executed again.
function initEditorJS(options: EditorConfig) {
return new Promise<EditorJS>((resolve, reject) => {
const editor = new EditorJS(options)
editor.isReady
.then(() => {
console.log(`${options.holder} Editor.js is ready to work!`)
resolve(editor)
})
.catch(reason => {
console.error(`${options.holder} Editor.js initialization failed because of ${reason}`)
reject(reason)
})
})
}
useEffect(() => {
if (!editorRef.current) {
renderEditor()
}
return () => {
editorRef.current?.destroy()
editorRef.current = null
}
}, [])
function renderEditor() {
if (loading) {
return
}
setLoading(true)
initEditorJS({
holder: editorId,
tools,
data,
})
.then(editor => {
editorRef.current = editor
})
.catch(reason => {
message.error(`Editor.js initialization failed because of ${reason}`)
})
.finally(() => {
setLoading(false)
})
}
Although I have done these, Editorjs still instantiated twice.
I successfully avoided rendering twice with the following code:
// Editor.tsx
import { useEffect, useRef, useState } from 'react';
import EditorJS, { type LogLevels, type OutputData } from '@editorjs/editorjs';
import Header from '@editorjs/header';
import _ from 'lodash';
const DEFAULT_INITIAL_DATA: OutputData = {
time: new Date().getTime(),
blocks: [
{
type: 'header',
data: {
text: 'This is my awesome editor!',
level: 1,
},
},
],
};
const EDITOR_HOLDER_ID = 'editorjs';
const Editor = (/* _props: any */) => {
const ejInstance = useRef<EditorJS | null>();
const [editorData, setEditorData] = useState(() => {
return DEFAULT_INITIAL_DATA;
});
const initEditor = () => {
const editor = new EditorJS({
holder: EDITOR_HOLDER_ID,
logLevel: 'ERROR' as LogLevels,
data: editorData,
// onReady: () => {
// ejInstance.current = editor;
// },
onChange: async (api, _event) => {
const content = await api.saver.save();
setEditorData(content);
},
autofocus: true,
tools: {
header: Header,
},
});
return editor;
};
// This will run only once
useEffect(() => {
if (_.isNil(ejInstance.current)) {
const editor = initEditor();
ejInstance.current = editor;
}
return () => {
if (ejInstance.current?.destroy) {
ejInstance.current?.destroy();
ejInstance.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div id={EDITOR_HOLDER_ID} />;
};
export default Editor;
I have a question, here in my code when i set the ref to the editor inside the onReady function in the editor config, it results in double codex editor but when i use the below code (i.e the ref's value is set to the editor after the initialization of EditorJS) it works properly without duplicating the codex editor.
I think this is happening becuase when i use the ref.current = editor inside the onReady function, it emits the event twice
code:
import EditorJS from "@editorjs/editorjs";
import Header from "@editorjs/header";
import LinkTool from "@editorjs/link";
import SimpleImage from "@editorjs/simple-image";
import Checklist from "@editorjs/checklist";
import Embed from "@editorjs/embed";
import Table from "@editorjs/table";
import List from "@editorjs/list";
import Code from "@editorjs/code";
import InlineCode from "@editorjs/inline-code";
import Marker from "@editorjs/marker";
import DragDrop from "editorjs-drag-drop";
import TextareaAutosize from "react-textarea-autosize";
import { toast } from "sonner";
import { Button } from "./ui/button";
import { useEffect, useRef, useState } from "react";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
export default function Editor() {
const ref = useRef<EditorJS>();
const [isSaving, setIsSaving] = useState(false);
async function handleClick() {
if (ref.current) {
setIsSaving(true);
const output = await ref.current.save();
console.log(output);
toast("Saved!");
setTimeout(() => setIsSaving(false), 1000);
}
}
useEffect(() => {
if (!ref.current) {
const editor = new EditorJS({
holder: "editor",
tools: {
header: {
class: Header,
},
linkTool: {
class: LinkTool,
config: {
endpoint: "http://localhost:5173/fetchUrl",
},
},
image: SimpleImage,
checklist: {
class: Checklist,
inlineToolbar: true,
},
list: List,
code: Code,
inlineCode: InlineCode,
table: Table,
embed: Embed,
marker: {
class: Marker,
shortcut: "CMD+M",
},
},
onReady: () => {
// ref.current = editor;
new DragDrop(editor);
toast("Editor is ready!");
},
autofocus: true,
});
ref.current = editor;
return () => {
if (ref.current?.destroy) {
ref.current?.destroy();
ref.current = undefined;
}
};
}
}, []);
useEffect(() => {
const handleKeyDowmn = (e: KeyboardEvent) => {
if (e.ctrlKey && e.key === "s") {
e.preventDefault();
handleClick();
}
};
window.addEventListener("keydown", handleKeyDowmn);
return () => {
window.removeEventListener("keydown", handleKeyDowmn);
};
}, []);
return (
<div className="container m-4 space-y-8">
<div className="grid w-full gap-10">
<div className="flex w-full items-center justify-between">
<Button type="submit" onClick={handleClick}>
{isSaving && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
<span>Save</span>
</Button>
</div>
<div className="prose prose-stone mx-auto w-[800px] dark:prose-invert">
<TextareaAutosize
autoFocus
id="title"
placeholder="Post title"
className="w-full resize-none appearance-none overflow-hidden bg-transparent text-5xl font-bold focus:outline-none"
/>
<div id="editor" className="min-h-[500px]" />
<p className="text-sm text-gray-500">
Use{" "}
<kbd className="rounded-md border bg-muted px-1 text-xs uppercase">
Tab
</kbd>{" "}
to open the command menu.
</p>
</div>
</div>
</div>
);
}