Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue: Multiple Instances of EditorJS Created in React with Vite and TypeScript #2731

Closed
imran1khan opened this issue Jun 1, 2024 · 7 comments
Labels

Comments

@imran1khan
Copy link

imran1khan commented Jun 1, 2024

I am experiencing an issue with EditorJS in my React application using Vite and TypeScript. Specifically, multiple instances of the editor are being created upon the first mount, even though I intend to create only one instance and reuse it. Additionally, I'm encountering errors when attempting to destroy the editor instance.

Environment
EditorJS Version:

"@editorjs/checklist": "^1.6.0",
"@editorjs/editorjs": "^2.29.1",
"@editorjs/header": "^2.8.1",
 "@editorjs/list": "^1.9.0",

React Version:

"react": "^18.2.0",

Vite Version:

"vite": "^5.2.0"
"typescript": "^5.2.2"

Steps to Reproduce

1. i initialize EditorJS in a separate configuration file.
2. I import and use the initialized EditorJS instance in my React component.
3. Upon the first render, two instances of EditorJS are created.
4. On subsequent renders, additional instances are sometimes created.
5. I attempt to destroy the EditorJS instance using editor.clear(), but encounter errors indicating that editor.destroy is not a function.

Code Snippets

editorConfig.ts:

import EditorJs from '@editorjs/editorjs';
import Header from "@editorjs/header";
import List from "@editorjs/list";
import CheckList from "@editorjs/checklist";

let editorInstance: EditorJs | null = null;

export const initializeEditor = (holder: string): EditorJs => {
  if (!editorInstance) {
    editorInstance = new EditorJs({
      holder,
      placeholder: "Let's write an awesome story!",
      tools: {
        header: {
          class: Header,
          inlineToolbar: true,
          shortcut: 'CMD+SHIFT+H',
        },
        list: List,
        checklist: CheckList,
      },
    });
  }
  return editorInstance;
};

export const getEditorInstance = (): EditorJs | null => editorInstance;

export const destroyEditor = (): void => {
  if (editorInstance) {
    editorInstance.clear();
    editorInstance = null;
  }
};

Editor.tsx:

import { useEffect, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { SaveData } from '../store/SaveData';
import { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types';
import { AppState } from '@excalidraw/excalidraw/types/types';
import { initializeEditor, getEditorInstance, destroyEditor } from '../editorConfig';

interface Prop {
  whiteBoardRef: MutableRefObject<ExcalidrawElement[]>,
  appStateRef: MutableRefObject<AppState[] | undefined>,
}

function Editor({ whiteBoardRef, appStateRef }: Prop) {
  const saveData = useRecoilValue(SaveData);
  const initialized = useRef(false);

  useEffect(() => {
    if (!initialized.current) {
      initialized.current = true;
      const editor = initializeEditor('editorjs');

      editor.isReady.then(() => {
        console.log('EditorJS is ready');
      });

      return () => {
        destroyEditor();
        initialized.current = false;
      };
    }
  }, []);

  useEffect(() => {
    const saveDataInBackend = async () => {
      try {
        const editor = getEditorInstance();
        const fileData = await editor?.save();
        console.log(fileData);
        const response = await fetch('http:https://localhost:3000/upload', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            excalidrawElements: JSON.stringify(whiteBoardRef.current),
            appState: JSON.stringify(appStateRef.current),
            fileData: JSON.stringify(fileData),
          }),
        });
        console.log(await response.json());
      } catch (error) {
        console.log(error);
      }
    };

    if (saveData) {
      saveDataInBackend();
    }
  }, [saveData, whiteBoardRef, appStateRef]);

  return (
    <div id='docDiv' className='h-full w-[50%]'>
      <div id='editorjs'></div>
    </div>
  );
}

export default Editor;

Expected Behavior

Only one instance of EditorJS should be created upon the first mount.

Subsequent renders should not create additional instances.

The editor.clear() method should effectively clear and destroy the EditorJS instance without errors.

Actual Behavior

Two instances of EditorJS are created upon the first mount.

Additional instances are sometimes created on subsequent renders.

Errors occur when attempting to destroy the EditorJS instance using editor.clear().

Additional Information

I am using Vite for bundling and TypeScript for type checking. 

Any guidance on how to ensure only one instance of EditorJS is created and properly managed would be greatly appreciated.

Device : lenove destop
Browser : chrome
OS: windows

@imran1khan imran1khan added the bug label Jun 1, 2024
@Vishvsalvi
Copy link

Remove react strictmode to avoid double instances

@imran1khan
Copy link
Author

@Vishvsalvi , bro i am sorry to say but it's still not working, every single time when it re-rander it creates new instance of Editorjs

@hsnfirdaus
Copy link

I'm using hooks, it's working:

import { useEffect, useRef, useState } from "react";
import EditorJS from "@editorjs/editorjs";
import { EditorConfig } from "@editorjs/editorjs/types/configs";
import { DEFAULT_EDITORJS_TOOLS } from "@/lib/editorjs/editorjs";

const useEditor = ({ onDestroy, ...config }: EditorConfig & { onDestroy?: () => void }) => {
	const [isEditorReady, setIsEditorReady] = useState(false);
	const editorInstance = useRef<EditorJS>(null);

	useEffect(() => {
		if (!editorInstance.current) {
			let dom: HTMLElement | undefined = undefined;
			if (config.holder instanceof HTMLElement) {
				dom = config.holder;
			} else if (config.holder) {
				const el = document.getElementById(config.holder);
				if (el) {
					dom = el;
				}
			}
			if (dom !== undefined) {
				const editorJsDOM = document.createElement("div");
				dom.appendChild(editorJsDOM);
				editorInstance.current = new EditorJS({
					tools: DEFAULT_EDITORJS_TOOLS,
					...config,
					holder: editorJsDOM,
					onReady: () => {
						setIsEditorReady(true);
						config.onReady?.();
					},
				});
			}
		}
		return () => {
			if (editorInstance.current && editorInstance.current.destroy) {
				editorInstance.current.destroy();
				editorInstance.current = null;
				onDestroy?.();
			}
		};
	}, []);

	return {
		isEditorReady,
		editor: editorInstance.current,
	};
};

export default useEditor;

@imran1khan
Copy link
Author

@hsnfirdaus hello, bro can you share the full code with me, that would be verry helpfull,

const useEditor = ({ onDestroy, ...config }: EditorConfig & { onDestroy?: () => void }) => {
	const [isEditorReady, setIsEditorReady] = useState(false);
	const editorInstance = useRef<EditorJS>(null);

and please clearify this for me, where this { onDestroy, ...config is comming from and one more thing is
import { DEFAULT_EDITORJS_TOOLS } from "@/lib/editorjs/editorjs"; and what are these

@imran1khan
Copy link
Author

@hsnfirdaus hi, can we talk in private. it would be helpfull if you can share your emil or something

@hsnfirdaus
Copy link

@hsnfirdaus hello, bro can you share the full code with me, that would be verry helpfull,

const useEditor = ({ onDestroy, ...config }: EditorConfig & { onDestroy?: () => void }) => {
	const [isEditorReady, setIsEditorReady] = useState(false);
	const editorInstance = useRef<EditorJS>(null);

and please clearify this for me, where this { onDestroy, ...config is comming from and one more thing is import { DEFAULT_EDITORJS_TOOLS } from "@/lib/editorjs/editorjs"; and what are these

I'm using typescript, this is simplified example of my previous reply useEditor.ts. config is configuration of editorjs (holder, tools, etc.):

import { useEffect, useRef, useState } from "react";
import EditorJS from "@editorjs/editorjs";
import { EditorConfig } from "@editorjs/editorjs/types/configs";

const useEditor = (config: EditorConfig) => {
	const [isEditorReady, setIsEditorReady] = useState(false);
	const editorInstance = useRef<EditorJS>(null);

	useEffect(() => {
		if (!editorInstance.current) {
			editorInstance.current = new EditorJS({
				...config,
				onReady: () => {
					setIsEditorReady(true);
					config.onReady?.();
				},
			});
		}
		return () => {
			if (editorInstance.current && editorInstance.current.destroy) {
				editorInstance.current.destroy();
				editorInstance.current = null;
			}
		};
	}, []);

	return {
		isEditorReady,
		editor: editorInstance.current,
	};
};

export default useEditor;

Usage (App.tsx):

import { EditorConfig } from "@editorjs/editorjs/types/configs";
import Paragraph from "@editorjs/paragraph";
import Header from "@editorjs/header";
import useEditor from "./useEditor";

const config: EditorConfig = {
  holder: 'editorjs',
  tools: {
    paragraph: Paragraph,
    header: Header
  }
} 

function App() {
  const { editor, isEditorReady } = useEditor(config);

  return (
      <div>
        <div id="editorjs"></div>
      </div>
  )
}

export default App

editor and isEditorReady variable is useful when you want to dynamicly change the content of editor (eg. After fetching content from server)

@imran1khan
Copy link
Author

@hsnfirdaus thanks bro, working perfectly,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants