suren-atoyan / monaco-react

Monaco Editor for React - use the monaco-editor in any React application without needing to use webpack (or rollup/parcel/etc) configuration files / plugins
https://monaco-react.surenatoyan.com/
MIT License
3.69k stars 264 forks source link

[BUG] Element already has context attribute #378

Open UrSok opened 2 years ago

UrSok commented 2 years ago

Describe the bug When using React 18 there is a chance that two editors will be mounted and an exception will be thrown making the use of editor impossible.

Screenshots The error image

Two editors image

jseparovic commented 2 years ago

I'm seeing this issue too. Did you happen to find a workaround?

Randomly I see 2 or 3 editors rendered one after the other, and there is a matching error in the logs for each duplicate editor:

react_devtools_backend.js:4026 Element already has context attribute 
    at Editor (webpack-internal:///./node_modules/@monaco-editor/react/lib/es/Editor/Editor.js:27:3)
    at div
jseparovic commented 2 years ago

I think I found a workaround for this issue. If you put a small delay in the rendering of the Editor it seems to not be affected by the issue in my setup.

    const [loading, setLoading] = useState<boolean>(true);

    useEffect(() => {
        setTimeout(() => {
            setLoading(false);
        }, 250);
    }, [])

    if(loading) {
        return <></>
    }

    return (
        <div style={{border: "1px solid #ccc"}} className={className}>
            <Editor
                options={{

Not ideal, but better than a duplicate non functioning editor components.

jseparovic commented 2 years ago

@suren-atoyan any ideas on this one? I still randomly see this issue with the workaround on load timeout set.

adobs commented 2 years ago

+1 to this issue @suren-atoyan. Thx

samblackk commented 2 years ago

+1 to this issue @suren-atoyan

suren-atoyan commented 2 years ago

is it possible to create a reproducible snippet or it happens unpredictably?

jseparovic commented 2 years ago

@suren-atoyan Here is the component I'm using and it happens every time (with the 250ms delay commented out)

import React, {FC, useEffect, useState} from 'react';

import * as monaco from "monaco-editor";
import Editor, {loader, Monaco,} from "@monaco-editor/react";
import {setDiagnosticsOptions} from 'monaco-yaml';
import {editor} from "monaco-editor";

loader.config({ monaco });

// @ts-ignore
window.MonacoEnvironment = {
    getWorker(moduleId: any, label: string) {
        switch (label) {
            case 'editorWorkerService':
                // @ts-ignore
                return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker', import.meta.url));
            case 'css':
            case 'less':
            case 'scss':
                // @ts-ignore
                return new Worker(new URL('monaco-editor/esm/vs/language/css/css.worker', import.meta.url));
            case 'handlebars':
            case 'html':
            case 'razor':
                return new Worker(
                    // @ts-ignore
                    new URL('monaco-editor/esm/vs/language/html/html.worker', import.meta.url),
                );
            case 'json':
                return new Worker(
                    // @ts-ignore
                    new URL('monaco-editor/esm/vs/language/json/json.worker', import.meta.url),
                );
            case 'javascript':
            case 'typescript':
                return new Worker(
                    // @ts-ignore
                    new URL('monaco-editor/esm/vs/language/typescript/ts.worker', import.meta.url),
                );
            case 'yaml':
                // @ts-ignore
                return new Worker(new URL('monaco-yaml/yaml.worker', import.meta.url));
            default:
                throw new Error(`Unknown label ${label}`);
        }
    },
};

interface CodeEditorProps {
    language: string;
    value: any;
    disabled?: boolean;
    onChange(value: string|undefined): void;
    className?: string;
    placeholder?: any;
    width?: string;
    height?: string;
    fontSize?: number;
    actions?: Array<editor.IActionDescriptor>;
}

export const CodeEditor: FC<CodeEditorProps> = (props) => {
    const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor>();
    const {language, value, disabled, onChange, className, width, height, fontSize, actions} = props;

    const handleOnChange = (value: string|undefined) => {
        onChange(value);
    }

    const handleEditorValidation = (markers: any) => {
        // model markers
        markers.forEach((marker: any) => console.log("onValidate:", marker.message));
    }

    const handleOnMount = (
        editor: monaco.editor.IStandaloneCodeEditor,
        monaco: Monaco,
    ) => {
        setEditor(editor);
        if(editor && actions) {
            for(const action of actions) {
                editor.addAction(action);
            }
        }
    }

    useEffect(() => {
        setDiagnosticsOptions({
            // Have to set an empty Diagnostics options to get syntax checking
            enableSchemaRequest: true,
            hover: true,
            completion: true,
            validate: true,
            format: true,
        });

        return () => {
            editor && editor.dispose();
        };
    }, [])

    /*
    const [loading, setLoading] = useState<boolean>(true);

    useEffect(() => {
        setTimeout(() => {
            setLoading(false);
        }, 250);
    }, [])

    if(loading) {
        return <></>
    }
    */

    return (
        <div style={{border: "1px solid #ccc"}} className={className}>
            <Editor
                options={{
                    readOnly: disabled,
                    lineDecorationsWidth: 5,
                    lineNumbersMinChars: 0,
                    glyphMargin: true,
                    folding: true,
                    lineNumbers: 'on',
                    minimap: {
                        enabled: false,
                    },
                    fontSize: fontSize || 11,
                    quickSuggestions: {
                        other: false,
                        comments: false,
                        strings: false,
                    },
                    parameterHints: {
                        enabled: false,
                    },
                    suggestOnTriggerCharacters: true,
                    acceptSuggestionOnEnter: "on",
                    tabCompletion: "on",
                    wordBasedSuggestions: false,
                    scrollbar: {
                        alwaysConsumeMouseWheel: false,
                    },
                    contextmenu: true,
                }}
                width={width}
                height={height}
                language={language}
                value={value || ""}
                onValidate={handleEditorValidation}
                onChange={handleOnChange}
                onMount={handleOnMount}
            />
        </div>
    );
}

Here is the webpack I'm using:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const { outputConfig, copyPluginPatterns, entryConfig, devServer } = require("./env.config");
const {SourceMapDevToolPlugin} = require("webpack");
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');

const APP_DIR = path.resolve(__dirname, './src');
const MONACO_DIR = path.resolve(__dirname, './node_modules/monaco-editor');
const ANIMATE_DIR = path.resolve(__dirname, './node_modules/animate.css');

module.exports = (env, options) =>
{
    return {
        mode: options.mode,
        entry: entryConfig,
        devServer,
        devtool: 'eval-source-map',
        target: "web",
        module: {
            parser: {
                javascript: {
                    wrappedContextRegExp: /.*/,
                    wrappedContextRecursive: true
                }
            },
            rules: [
                //{
                //    test: /\.css$/i,
                //    use: ['style-loader', 'css-loader'],
                //},
                {
                    test: /codicon\.ttf$/,
                    use: [{
                        loader: "file-loader",
                        options: {
                            name: "[name].[ext]",
                            publicPath: "https://server.com/resources"
                        }
                    }]
                },
                {
                    test: /\.css$/i,
                    include: APP_DIR,
                    use: [{
                        loader: 'style-loader',
                    }, {
                        loader: 'css-loader',
                        options: {
                            modules: true,
                            namedExport: true,
                        },
                    }],
                },
                {
                    test: /\.css$/,
                    include: MONACO_DIR,
                    use: ['style-loader', 'css-loader'],
                },
                {
                    test: /\.css$/,
                    include: ANIMATE_DIR,
                    use: ['style-loader', 'css-loader'],
                },
                {
                    test: /\.svg$/,
                    type: "javascript/auto",
                    loader: "file-loader",
                    options: {
                        name: "[path][name].[ext]",
                        context: path.resolve(__dirname, "node_modules"),
                        emitFile: true,
                    },
                },
                {
                    test: /\.(ts|tsx)$/,
                    loader: "ts-loader",
                    options: {
                        allowTsInNodeModules: true,
                    }
                },
                {
                    test: /\.scss$/,
                    use: [
                        // We're in dev and want HMR, SCSS is handled in JS
                        // In production, we want our css as files
                        "style-loader",
                        "css-loader",
                        {
                            loader: "postcss-loader",
                            options: {
                                postcssOptions: {
                                    plugins: [
                                        ["postcss-preset-env"],
                                    ],
                                },
                            },
                        },
                        "sass-loader"
                    ],
                },
                {
                    test: /\.(?:ico|gif|png|jpg|jpeg|svg)$/i,
                    type: "javascript/auto",
                    loader: "file-loader",
                    options: {
                        publicPath: "../",
                        useRelativePaths: true,
                        name: "[path][name].[ext]",
                        context: path.resolve(__dirname, "src/assets"),
                        emitFile: false,
                    },
                },
                {
                    test: /\.(woff(2)?|eot|ttf|otf|svg)$/,
                    type: "javascript/auto",
                    exclude: /images/,
                    loader: "file-loader",
                    options: {
                        publicPath: "../",
                        context: path.resolve(__dirname, "src/assets"),
                        name: "[path][name].[ext]",
                        emitFile: false,
                    },
                },
            ],
        },
        resolve: {
            extensions: [".webpack.js", ".web.js", ".tsx", ".ts", ".js", ".css", ".svg"],
            modules: ['node_modules'],
        },
        output: {
            filename: "js/[name].bundle.js",
            path: path.resolve(__dirname, outputConfig.destPath),
        },
        plugins: [
            new HtmlWebpackPlugin({
                template: "./src/index.html",
                inject: true,
                minify: false,
                publicPath: "/",
            }),
            new CopyPlugin(copyPluginPatterns),
            //new SourceMapDevToolPlugin({}),
            new MonacoWebpackPlugin({
                // available options are documented at https://github.com/Microsoft/monaco-editor-webpack-plugin#options
                languages: ['json', 'javascript', 'typescript']
            })
        ],
    };
};
jseparovic commented 2 years ago
image
suren-atoyan commented 2 years ago

@samblackk @adobs @UrSok could you please provide more information about your environment? What do you use CRA, Vite, or something else? If you have a custom webpack config it would be nice if you could share it as well.

Just in case, I put @jseparovic's code (shared above) in the Vite environment (with React 18) and everything works as expected.

adobs commented 2 years ago
import React, { useState } from 'react';

import MonacoEditor, { EditorProps, loader } from '@monaco-editor/react';
import { Box, useColorMode, useTheme } from '@chakra-ui/react';
import setTheme from './editor-theme';

loader.config({
  paths: {
    vs: '/monaco-editor',
  },
});

interface Props extends EditorProps {
  readOnly?: boolean;
  onTaskRun?: Function;
}

const CodeEditor = ({ readOnly, onTaskRun, ...otherProps }: Props) => {
  const [height, setHeight] = useState(0);
  const { colorMode } = useColorMode();
  const theme = useTheme();

  const editorDidMount = (editor: any, monaco: any) => {
    setTheme(monaco, theme.colors, colorMode);

    const blockContext =
      'editorTextFocus && !suggestWidgetVisible && !renameInputVisible && !inSnippetMode ' +
      '&& !quickFixWidgetVisible';

    editor.addAction({
      id: 'runCell',
      label: 'Run Cell',
      // eslint-disable-next-line no-bitwise
      keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
      precondition: blockContext,
      run: () => (onTaskRun ? onTaskRun() : null),
    });

    const setDynamicHeight = () => {
      // get height of new content
      const contentHeight = editor.getContentHeight();

      // set new height according to conditions:
      // content height is at least 100px
      // content height is at most 400px
      const newHeight = Math.min(Math.max(contentHeight, 100), 400);
      if (contentHeight !== height) {
        setHeight(newHeight);
      }
    };

    editor.onDidContentSizeChange(setDynamicHeight);
    setDynamicHeight();
  };

  return (
    <Box height={`${height}px`}>
      <MonacoEditor
        {...otherProps}
        theme="light"
        onMount={editorDidMount}
        options={{
          lineDecorationsWidth: '0ch',
          scrollBeyondLastLine: false,
          minimap: { enabled: false },
          readOnly,
        }}
      />
    </Box>
  );
};

export default CodeEditor;

webpack config

const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const path = require('path');

// Env/Config
const { localEnv, nginxTemplateVariables } = require('./env');

// look for monaco-editor package folder
const monacoRoot = path.dirname(require.resolve('monaco-editor/package.json'));

const DEVELOPMENT = process.env.NODE_ENV !== 'production';

module.exports = merge(spaConfig, {
  entry: './src/index',
  devServer: {
    port: 5000,
    static: {
      directory: path.join(__dirname, 'dist'),
    },
  },
  output: {
    publicPath: '/',
  },
  resolve: {
    alias: {
      root: path.resolve(__dirname),
      src: path.resolve(__dirname, 'src'),
      api: path.resolve(__dirname, 'src/api'),
      components: path.resolve(__dirname, 'src/components'),
      containers: path.resolve(__dirname, 'src/containers'),
      interfaces: path.resolve(__dirname, 'src/interfaces'),
      providers: path.resolve(__dirname, 'src/providers'),
      utils: path.resolve(__dirname, 'src/utils'),
      pages: path.resolve(__dirname, 'src/pages'),
      mocks: path.resolve(__dirname, 'src/mocks'),
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './templates/index.ejs',
      templateParameters: DEVELOPMENT ? localEnv : nginxTemplateVariables,
    }),
    new CopyPlugin({
      patterns: [
        { from: './public', to: '.' },
        { from: path.join(monacoRoot, 'min/vs'), to: 'monaco-editor' },
      ],
    }),
  ],
});

in package.json

    "@monaco-editor/react": "^4.3.1",
    "monaco-editor": "^0.33.0",
adobs commented 2 years ago

EDIT upgraded to

    "@monaco-editor/react": "^4.4.5",

and issue still persists

UrSok commented 2 years ago

Hi, sorry for the delay. I'm no longer working on the project where I've been using this package. I was using the following cra: react-boilerplate-cra-template I'm not very familiar with the webpack, but I was using the default config provided by the mentioned template.

I created a wrapper component where I use the editor:

import Editor, { Monaco, OnChange } from '@monaco-editor/react';
import monaco from 'monaco-editor';
import React, { MutableRefObject } from 'react';
import styled from 'styled-components';

import LoadingSpinner from '../../LoadingSpinner';

type CodeEditorProps = {
  editorRef?: MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined>;
  language: string;
  width?: string | number;
  height?: string | number;
  defaultValue?: string;
  value?: string;
  readOnly?: boolean;
  onModelChange?: OnChange;
};

export default function CodeEditor(props: CodeEditorProps) {
  const {
    editorRef,
    language,
    width,
    height,
    defaultValue,
    value,
    readOnly,
    onModelChange,
  } = props;

  const onMount = (
    editor: monaco.editor.IStandaloneCodeEditor,
    monaco: Monaco,
  ) => {
    if (!editorRef) return;
    editorRef.current = editor;
  };

  return (
    <EditorWrapper>
      <Editor
        loading={<LoadingSpinner />}
        height={height ?? 300}
        width={width}
        language={language}
        defaultValue={defaultValue}
        value={value}
        options={{
          readOnly: readOnly,
        }}
        onMount={onMount}
        onChange={onModelChange}
      />
    </EditorWrapper>
  );
}

const EditorWrapper = styled.div`
  border: 1px solid #d9d9d9;
  border-radius: 2px;
  section {
    resize: vertical;
    overflow: auto;
  }
`;

One of the places where I use this wrapper component is here:

import { Space, Typography } from 'antd';
import CodeEditor from 'app/components/Input/CodeEditor';
import LanguageSelect from 'app/components/Input/LanguageSelect';
import { Language } from 'app/types/enums/language';
import React, { MutableRefObject, useEffect, useState } from 'react';
import monaco from 'monaco-editor';
import { stubInputLanguage } from 'config/monaco';
import { useWatch } from 'antd/lib/form/Form';
import { stubGeneratorApi } from 'app/api/stubGenerator';
import ErrorAlert from './components/ErrorAlert';
import { FormFields } from '../../types';
import { skipToken } from '@reduxjs/toolkit/dist/query';

type StubGeneratorProps = {
  stubCodeEditorRef: MutableRefObject<
    monaco.editor.IStandaloneCodeEditor | undefined
  >;
  disabled?: boolean;
  initialValue?: string;
  onStubInputChangedDecorator?: (value: string | undefined) => void;
  onResultChanged?: (
    stubInput: string | undefined,
    generatedStub: string | undefined,
    isValid: boolean,
  ) => void;
};

export default function StubGenerator(props: StubGeneratorProps) {
  const {
    stubCodeEditorRef,
    disabled,
    initialValue,
    onStubInputChangedDecorator,
    onResultChanged,
  } = props;

  const [input, setInput] = useState(initialValue);
  const language: Language = useWatch(FormFields.stubLanguage);

  const { data: generatorResult } = stubGeneratorApi.useGenerateStubQuery(
    language
      ? {
          language,
          input,
        }
      : skipToken,
  );

  const handleOnStubInputChange = async (
    value: string | undefined,
    ev: monaco.editor.IModelContentChangedEvent,
  ) => {
    setInput(value);
    if (onStubInputChangedDecorator) {
      onStubInputChangedDecorator(value);
    }
  };

  const handleOnStubResultChange = async (
    value: string | undefined,
    ev: monaco.editor.IModelContentChangedEvent,
  ) => {
    if (!generatorResult) return;

    const isEmpty =
      !generatorResult.value ||
      (generatorResult.value && !generatorResult.value.stub) ||
      (generatorResult.value && generatorResult.value.stub?.length === 0);

    const isValid =
      !isEmpty || (generatorResult.isSuccess && !generatorResult.value?.error);

    if (onResultChanged) {
      onResultChanged(input, generatorResult.value?.stub, isValid);
    }
  };

  return (
    <>
      <Space
        direction="vertical"
        style={{
          width: '100%',
        }}
      >
        <CodeEditor
          editorRef={stubCodeEditorRef}
          defaultValue={initialValue}
          language={stubInputLanguage}
          readOnly={disabled}
          onModelChange={handleOnStubInputChange}
        />

        {generatorResult &&
          !generatorResult.isSuccess &&
          generatorResult.value &&
          generatorResult.value.error && (
            <ErrorAlert error={generatorResult.value.error!} />
          )}
      </Space>
      <Typography.Text strong>Result</Typography.Text>
      <LanguageSelect
        antdFieldName={FormFields.stubLanguage}
        placeholder="Generation language"
        width="sm"
        defaultLanguage={Language.javascript}
        style={{
          marginBottom: '5px',
        }}
      />
      <CodeEditor
        language={language}
        defaultValue="// update the stub input to get the result"
        value={generatorResult?.value?.stub}
        onModelChange={handleOnStubResultChange}
        readOnly
      />
    </>
  );
}

Hope this helps.

UrSok commented 2 years ago

I also tried to reproduce the issue using a new project, but it worked as expected.

jseparovic commented 2 years ago

@suren-atoyan I tried my code in Vite also but I still get the issue.

It's intermittent, but I seem to be able to reproduce it if I'm on another page loading some data and then switch to the editor page while the other page is still loading.

Is there any cleanup that should be done on the editor when it unmounts?

jseparovic commented 2 years ago

@suren-atoyan just realized I'm already doing editor.dispose(); on unmount

Tchaikovsky1114 commented 2 years ago

i got the same error

import Editor, { Monaco } from '@monaco-editor/react';
import * as monaco from '../../node_modules/monaco-editor/esm/vs/editor/editor.api';
import prettier from 'prettier';
import parser from 'prettier/parser-babel';
import { useRef } from 'react';

interface CodeEditorProps {
  onChange(value: string): void;
  content:string;
}

const CodeEditor = ({ onChange,content }: CodeEditorProps) => {
  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>();

  const onEditorBeforeMount = (monaco:Monaco) =>{
    monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true);
  }

  const onEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor,_monaco: Monaco) => {
    editorRef.current = editor;
    editor.getModel()?.updateOptions({ tabSize: 2 });
    editor.onDidChangeModelContent(() => onChange(editor.getValue()));
  };

  const onFormat = () => {
    if (editorRef.current) {
      const unformatted = editorRef.current.getValue();

      const formatting = prettier
        .format(unformatted, {
          parser: 'babel',
          plugins: [parser],
          useTabs: false,
          semi: true,
          singleQuote: true,
        })
        .replace(/\n$/, '');
      // setting values return
      editorRef.current?.setValue(formatting);
    }
  };
  return (
    <div className={`relative h-full w-full  bg-white group`}>
      <button
        className="text-sm group-hover:opacity-100 text-white absolute top-1 right-1 z-20 opacity-0 transition-opacity duration-300"
        onClick={onFormat}
      >
        Prettier
      </button>
      <Editor
      beforeMount={onEditorBeforeMount}
        onMount={onEditorDidMount}
        value={content}
        defaultLanguage="javascript"
        height="100%"
        theme="vs-dark"

        options={{    
          wordWrap: 'on',
          minimap: {
            enabled: false,
          },
          folding: false,
          lineNumbersMinChars: 2,
          fontSize: 16,
          scrollBeyondLastLine: false,
          automaticLayout: true,
        }}
      />
    </div>
  );
};

export default CodeEditor;
george-thomas-hill commented 2 years ago

I'm having this problem (intermittently) too.

suren-atoyan commented 2 years ago

@jseparovic

...but I seem to be able to reproduce it if I'm on another page loading some data and then switch to the editor page while the other page is still loading

could it be possible to reproduce your case here?

Tchaikovsky1114 commented 2 years ago

code-editor-error

code-editor-error2 gif

My guess is that there is something wrong with the initialization.

When this error occurs, several editor writing fields are created, Only the code entered in the lowest cell is executed.

suren-atoyan commented 2 years ago

@Tchaikovsky1114 thanks for the example.

I tried to reproduce it. here is the example when you load monaco source from CDN and this one when you use monaco-editor from node_modules.

Could you please check them and modify them according to your example so that I can see the issue? Thanks 🙌

Tchaikovsky1114 commented 2 years ago

i got ran to example to monaco-editor from node_modules on my environment with VSCODE .

the Error that Element already has context attribute is gone, but this error occure

0730 error d

What I do know is that if I don't write beforeMount, the Element already has context attribute error repeats hundreds of times.

george-thomas-hill commented 2 years ago

I didn't have this problem when I used webpack. It showed up when I switched to vite.

florian74 commented 2 years ago

I had the same problem, In my case I had to extract the Editor "options" part in one global constant (i.e export const MonacoOptions = { ... } and reuse it every where (i.e <Editor ... options={MonacoOptions} />.

Maybe worth a try ?

mprzybylski-agi commented 2 years ago

I had the same problem, In my case I had to extract the Editor "options" part in one global constant (i.e export const MonacoOptions = { ... } and reuse it every where (i.e <Editor ... options={MonacoOptions} />.

Maybe worth a try ?

this is what solved my Issue I had the same problem when using react-router with monaco-editor/react When switching between routes and going back to the one with monaco, then monaco would produce 2 or more models which broke some of my code. When I applied this solution with extracted options object to a const, it all started working as expected. Thanks florian72

jseparovic commented 2 years ago

I had the same problem, In my case I had to extract the Editor "options" part in one global constant (i.e export const MonacoOptions = { ... } and reuse it every where (i.e <Editor ... options={MonacoOptions} />.

Maybe worth a try ?

Is there a way to change the font using this approach? Or toggle the readonly flag?

suren-atoyan commented 2 years ago

Please someone provide me a reproducible snippet/repo.

Few thoughts related to the comments above.

Don't know how options are related to this bug (I still don't know the essence of the bug - I've reproduced it yet), but if options are not extracted from the component context then on every component update, a new instance of the options object will be created. Which will cause an update here.

function MyCoolComponent() {

  return (
    <Editor
      ...
      options={{ option1: ..., option2: ... }}
    />
  );
}

So, if extracting options does really help we should understand how editor.updateOptions related to the issue.

Is there a way to change the font using this approach? Or toggle the readonly flag?

instead of:

const EDITOR_OPTIONS = { a, b };

function MyCoolComponent() {

  return (
    <Editor
      ...
      options={EDITOR_OPTIONS}
    />
  );
}

you also can do this:

function MyCoolComponent() {
  const [isReadyOnly, setIsReadOnly] = useState();

  const options = useMemo(() => ({ a, b, isReadyOnly }), [isReadyOnly]);

  return (
    <Editor
      ...
      options={options}
    />
  );
}

This way you can use whatever you need from the component context at the same time update the object instance only if something is changed.

or

const EDITOR_BASE_OPTIONS = { a, b };

function MyCoolComponent() {
  const [isReadyOnly, setIsReadOnly] = useState();

  const options = useMemo(() => ({ ...EDITOR_BASE_OPTIONS, isReadyOnly }), [isReadyOnly]);

  return (
    <Editor
      ...
      options={options}
    />
  );
}
florian74 commented 2 years ago

When I tried to reproduce it on web ui like stackblitz I didn't succeed.

I have simplified the part of my project that cause the bug. I put it there: https://github.com/florian74/Monaco-playground

install with npm i, run with npm start, and click on the 3 dots button on the top right. you should see the double editor appear and the bug appear. I can reproduce it on my computer at least. I din't clean all dependencies though but it should not be the problem.

Hope it helps

suren-atoyan commented 2 years ago

@florian74 @jseparovic @mprzybylski-agi @george-thomas-hill @Tchaikovsky1114 @UrSok @adobs @samblackk please check the latest (v4.4.6) version - it should be fixed now.

jseparovic commented 2 years ago

@suren-atoyan Looks good to me. Thanks heaps for the fix. Cheers!

mateodelnorte commented 1 year ago

I'm getting this issue on version 4.4.6 and react 15.4.2.

SiebeVE commented 1 year ago

I'm also still having the issue with React 18.2, @monaco-editor/react 4.4.6, react-router-dom: 6.8.0.

It has the same behaviour as in florian74 repo that I put on codesandbox: https://codesandbox.io/p/github/SiebeVE/Monaco-playground/draft/charming-lamarr

When you click the 3 dots and the edit, it opens a modal that show 2 editors instead of 1.

EDIT: Mmh okay, when updating it to 4.4.6, that repo no longers reproduces the error, let me look some further if I can create a reproducable example.

SiebeVE commented 1 year ago

I was using the diff editor which didn't have the fix yet. @suren-atoyan if you got the time to check https://github.com/suren-atoyan/monaco-react/pull/448 so it is also fixed for the diff editor, that would be wonderfull!

stefan-kracht commented 1 year ago

Just FYI, I can confirm that I had the same issue in 4.3.1 and upgrading to 4.4.6 fixed it for me.