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

Issues with multiple instances on same page? [HELP WANTED] #504

Closed
kirasiris opened this issue May 18, 2023 · 3 comments
Closed

Issues with multiple instances on same page? [HELP WANTED] #504

kirasiris opened this issue May 18, 2023 · 3 comments
Labels

Comments

@kirasiris
Copy link

HEADS UP: This is an "I NEED HELP WITH MY CODE" type of post

I'm creating a html code editor(similar to codepen.io, bootsnipp.com, etc) and I need to use three(3) instances of the react-codemirror in the same page.

One would be for HTML; a second one for CSS and a third one for JavaScript. The way I use it is as follow:

const htmlExtension = [loadLanguage('html')]
const cssExtension = [loadLanguage('css')]
const javascriptExtension = [loadLanguage('javascript')]

const App = () => {
  return (
    <CodeEditor
      id="html"
      name="html"
      value={html}
      onChange={handleChange('html')}
      mode={htmlExtension}
      theme={`material`}
    />
  )
}

Furthermore, for the simplicity of the issue, I only pasted the HTML editor instance. The CodeEditor above makes references to the actual CodeMirror component and this one looks like this:

const CodeEditor = React.memo(
  ({ id = '', name = '', value, onChange, mode }) => {
    return (
      <CodeMirror
        id={id}
        name={name}
        value={value}
        height="200px"
        theme={vscodeDark}
        extensions={mode}
        onChange={onChange}
      />
    )
  }
)

I initially though the problem was caused due to the browser not being able to differentiate between the three Editors(I always go for the easiest solution) so I went and gave them a different ID to each one. To my surprise, the issue still remained.

I then decided to wrap my handleChange() in a useCallback() just as it looks below:

const [editorData, setEditorData] = useState({
  html: ``,
  css: ``,
  javascript: ``,
})

const { html, css, javascript } = editorData

const handleChange = useCallback(
  (name) => (e) => {
    setEditorData({ ...editorData, [name]: e })
  },
  [editorData]
)

const iframeRef = useRef(null)

useEffect(() => {
  const iframe = iframeRef.current
  const iframeDoc = iframe.contentDocument || iframe.contentWindow.document

  iframeDoc.open()
  iframeDoc.write(`
		  <html>
			<head>
			  <style>${css}</style>
			</head>
			<body>${html}</body>
			<script>${javascript}</script>
		  </html>
		`)
  iframeDoc.close()
}, [html, css, javascript])

I saw several posts that this was caused due to having the Editors update the same state over and over but I'm still unable to solve it. The solutions that I always find keep saying to wrap my functions in useCallback() and/or wrap the CodeMirror component in memo(). Any ideas?
The current behavior is this:
Click to see image on cloudinary
The image might not totally show my issue but whenever I type something in any of the editors, the state gets restarted? what I mean is something like "one click per character" I need to keep clicking on x editor to keep typing. For some reason, whenever I click on another editor, the state does definetely gets restarted and don't know why.

@jaywcjlove
Copy link
Member

@kirasiris

./index.js

import React, { createContext, useReducer } from "react";
import CodeMirror from "@uiw/react-codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { html } from "@codemirror/lang-html";
import { css } from "@codemirror/lang-css";
import { Iframe } from "./Iframe";

export const Context = createContext(null);
const initialData = {
  js: "console.log('hello world!');",
  css: "body {}",
  html: "<div>test</div>"
};

export default function App() {
  const [stateData, dispatch] = useReducer(
    (state, action) => ({ ...state, ...action }),
    initialData
  );

  return (
    <Context.Provider value={stateData}>
      <div>
        <CodeMirror
          value={stateData.js}
          height="200px"
          theme="dark"
          extensions={[javascript({ jsx: true })]}
          onChange={(value) => {
            dispatch({ ...stateData, js: value });
          }}
        />
        <CodeMirror
          value={stateData.html}
          height="200px"
          extensions={[html()]}
          onChange={(value, viewUpdate) => {
            dispatch({ ...stateData, html: value });
          }}
        />
        <CodeMirror
          value={stateData.css}
          height="200px"
          extensions={[css()]}
          onChange={(value, viewUpdate) => {
            dispatch({ ...stateData, css: value });
          }}
        />
        <Iframe />
      </div>
    </Context.Provider>
  );
}

./Iframe.js

import { useContext, useMemo, useRef } from "react";
import { Context } from "./App";

export const Iframe = () => {
  const frameRef = useRef(null);
  const data = useContext(Context);

  const jsString = data.js
    ? `<script type="text/javascript">${data.js}</script>`
    : "";
  const cssString = data.css ? `<style>${data.css}</style>` : "";
  const result = `<!DOCTYPE html><html><head>${cssString}</head><body>${data.html}</body>${jsString}</html>`;

  useMemo(() => {
    const blob = new Blob([result], { type: "text/html" });
    if (frameRef.current) {
      frameRef.current.src = URL.createObjectURL(blob);
    }
  }, [result]);
  return <iframe ref={frameRef} title="Test"></iframe>;
};

@kirasiris
Copy link
Author

Wow, @jaywcjlove, you actually gave me the whole solution, big thanks!.
This works exactly as I wanted!.

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

2 participants