Open UrSok opened 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
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.
@suren-atoyan any ideas on this one? I still randomly see this issue with the workaround on load timeout set.
+1 to this issue @suren-atoyan. Thx
+1 to this issue @suren-atoyan
is it possible to create a reproducible snippet or it happens unpredictably?
@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']
})
],
};
};
@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.
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",
EDIT upgraded to
"@monaco-editor/react": "^4.4.5",
and issue still persists
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.
I also tried to reproduce the issue using a new project, but it worked as expected.
@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?
@suren-atoyan just realized I'm already doing editor.dispose(); on unmount
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;
I'm having this problem (intermittently) too.
@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?
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.
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
d
What I do know is that if I don't write beforeMount, the Element already has context attribute
error repeats hundreds of times.
I didn't have this problem when I used webpack. It showed up when I switched to vite.
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 ?
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
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?
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}
/>
);
}
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
@florian74 @jseparovic @mprzybylski-agi @george-thomas-hill @Tchaikovsky1114 @UrSok @adobs @samblackk please check the latest (v4.4.6) version - it should be fixed now.
@suren-atoyan Looks good to me. Thanks heaps for the fix. Cheers!
I'm getting this issue on version 4.4.6 and react 15.4.2.
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.
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!
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.
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
Two editors