Skip to content

Commit fec8d06

Browse files
refactor: enhance code performance, maintainability and extendibility
replace the too many `useEffect` and `useCallbacks` with one `useEffect` to instantiate and update the editor and another effect to dispose the modelsSubscriptions when the component unmount
1 parent c62ec1f commit fec8d06

File tree

1 file changed

+104
-134
lines changed

1 file changed

+104
-134
lines changed

packages/wrapper-react/src/index.tsx

+104-134
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,120 @@
11
/* --------------------------------------------------------------------------------------------
2-
* Copyright (c) 2024 TypeFox and others.
3-
* Licensed under the MIT License. See LICENSE in the package root for license information.
4-
* ------------------------------------------------------------------------------------------ */
5-
6-
import * as monaco from '@codingame/monaco-vscode-editor-api';
7-
import React, { type CSSProperties, useCallback, useEffect, useRef } from 'react';
8-
import { didModelContentChange, MonacoEditorLanguageClientWrapper, type TextChanges, type TextModels, type WrapperConfig } from 'monaco-editor-wrapper';
2+
* Copyright (c) 2024 TypeFox and others.
3+
* Licensed under the MIT License. See LICENSE in the package root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
import React, { CSSProperties, useEffect, useRef } from "react"
6+
7+
import { IDisposable } from "monaco-editor"
8+
import {
9+
MonacoEditorLanguageClientWrapper,
10+
TextChanges,
11+
TextModels,
12+
WrapperConfig,
13+
didModelContentChange,
14+
} from "monaco-editor-wrapper"
915

1016
export type MonacoEditorProps = {
11-
style?: CSSProperties;
12-
className?: string;
13-
wrapperConfig: WrapperConfig,
14-
onTextChanged?: (textChanges: TextChanges) => void;
15-
onLoad?: (wrapper: MonacoEditorLanguageClientWrapper) => void;
16-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17-
onError?: (e: any) => void;
17+
style?: CSSProperties
18+
className?: string
19+
wrapperConfig: WrapperConfig
20+
onTextChanged?: (textChanges: TextChanges) => void
21+
onLoad?: (wrapper: MonacoEditorLanguageClientWrapper) => void
22+
onError?: (e: unknown) => void
1823
}
1924

25+
// this global variable is needed outside the react component because it will hold
26+
// the modelSubscriptions instance which is created when an instance of the editor is created
27+
// we need it as global because the instances are persistent even after the component is unmounted
28+
let modelSubscriptions: IDisposable[] = []
29+
30+
/**
31+
* A React component that renders a Monaco Editor given a wrapper config.
32+
*
33+
* @remarks
34+
*
35+
* This component is a thin wrapper around the Monaco Editor Language Client Wrapper.
36+
* It is responsible for rendering a Monaco Editor and setting up the language client.
37+
*
38+
* @param props - The props for the component.
39+
* @param props.style - The CSS style for the component.
40+
* @param props.className - The CSS class name for the component.
41+
* @param props.wrapperConfig - The configuration for the Monaco Editor Language Client Wrapper.
42+
* @param [props.onTextChanged] - A callback that is called when the text in the editor changes.
43+
* @param [props.onLoad] - A callback that is called when the editor has finished loading.
44+
* @param [props.onError] - A callback that is called when an error occurs while loading the editor.
45+
* @returns A React element that renders the Monaco Editor.
46+
*/
2047
export const MonacoEditorReactComp: React.FC<MonacoEditorProps> = (props) => {
21-
const {
22-
style,
23-
className,
24-
wrapperConfig,
25-
onTextChanged,
26-
onLoad,
27-
onError
28-
} = props;
29-
30-
const wrapperRef = useRef<MonacoEditorLanguageClientWrapper>(new MonacoEditorLanguageClientWrapper());
31-
const containerRef = useRef<HTMLDivElement>(null);
32-
const onTextChangedSubscriptions = useRef<monaco.IDisposable[]>([]);
33-
34-
useEffect(() => {
35-
return () => {
36-
destroyMonaco();
37-
};
38-
}, []);
39-
40-
useEffect(() => {
41-
handleReInit();
42-
}, [wrapperConfig]);
43-
44-
useEffect(() => {
45-
handleOnTextChanged();
46-
}, [onTextChanged]);
47-
48-
useEffect(() => {
49-
if (containerRef.current) {
50-
containerRef.current.className = className ?? '';
51-
wrapperConfig.htmlContainer = containerRef.current;
52-
}
53-
}, [className]);
48+
const { style, className, wrapperConfig, onTextChanged, onLoad, onError } = props
5449

55-
const handleReInit = useCallback(async () => {
56-
if (wrapperRef.current.isStopping() === undefined) {
57-
await destroyMonaco();
58-
} else {
59-
await wrapperRef.current.isStopping();
60-
}
50+
const containerRef = useRef<HTMLDivElement>(null)
6151

62-
if (wrapperRef.current.isStarting() === undefined) {
63-
await initMonaco();
64-
await startMonaco();
65-
} else {
66-
await wrapperRef.current.isStarting();
67-
}
52+
const editorRef = useRef<MonacoEditorLanguageClientWrapper>()
6853

69-
}, [wrapperConfig]);
54+
useEffect(() => {
55+
const editorContainerEl = containerRef.current
7056

71-
const initMonaco = useCallback(async () => {
72-
if (containerRef.current) {
73-
wrapperConfig.htmlContainer = containerRef.current;
74-
await wrapperRef.current.init(wrapperConfig);
75-
} else {
76-
throw new Error('No htmlContainer found! Aborting...');
77-
}
78-
}, [wrapperConfig]);
79-
80-
const startMonaco = useCallback(async () => {
81-
if (containerRef.current) {
82-
try {
83-
wrapperRef.current.registerModelUpdate((textModels: TextModels) => {
84-
if (textModels.modified !== undefined || textModels.original !== undefined) {
85-
const newSubscriptions: monaco.IDisposable[] = [];
86-
87-
if (textModels.modified !== undefined) {
88-
newSubscriptions.push(textModels.modified.onDidChangeContent(() => {
89-
didModelContentChange(textModels, wrapperConfig.editorAppConfig?.codeResources, onTextChanged);
90-
}));
91-
}
92-
93-
if (textModels.original !== undefined) {
94-
newSubscriptions.push(textModels.original.onDidChangeContent(() => {
95-
didModelContentChange(textModels, wrapperConfig.editorAppConfig?.codeResources, onTextChanged);
96-
}));
97-
}
98-
onTextChangedSubscriptions.current = newSubscriptions;
99-
// do it initially
100-
didModelContentChange(textModels, wrapperConfig.editorAppConfig?.codeResources, onTextChanged);
101-
}
102-
});
103-
104-
await wrapperRef.current.start();
105-
onLoad?.(wrapperRef.current);
106-
handleOnTextChanged();
107-
} catch (e) {
108-
if (onError) {
109-
onError(e);
110-
} else {
111-
throw e;
112-
}
113-
}
114-
} else {
115-
throw new Error('No htmlContainer found! Aborting...');
116-
}
117-
}, [onError, onLoad, onTextChanged]);
57+
if (!wrapperConfig || !editorContainerEl) return
11858

119-
const handleOnTextChanged = useCallback(() => {
120-
disposeOnTextChanged();
59+
if (wrapperConfig.editorAppConfig?.editorOptions && editorRef.current) {
60+
editorRef.current.getMonacoEditorApp()?.updateMonacoEditorOptions(wrapperConfig.editorAppConfig.editorOptions)
12161

122-
if (!onTextChanged) return;
62+
return
63+
}
12364

124-
}, [onTextChanged, wrapperConfig]);
65+
editorRef.current = new MonacoEditorLanguageClientWrapper()
12566

126-
const destroyMonaco = useCallback(async () => {
127-
try {
128-
await wrapperRef.current.dispose();
129-
} catch {
130-
// The language client may throw an error during disposal.
131-
// This should not prevent us from continue working.
132-
}
133-
disposeOnTextChanged();
134-
}, []);
67+
if (editorRef.current.isStopping() === undefined) {
68+
editorRef.current.dispose(true)
69+
} else {
70+
editorRef.current.isStopping()
71+
}
72+
73+
editorRef.current.init(wrapperConfig).then(() => {
74+
editorContainerEl.className = className ?? ""
75+
76+
try {
77+
editorRef.current?.registerModelUpdate((textModels: TextModels) => {
78+
if (textModels.modified != undefined) {
79+
modelSubscriptions.push(
80+
textModels.modified.onDidChangeContent(() => {
81+
didModelContentChange(textModels, wrapperConfig.editorAppConfig?.codeResources, onTextChanged)
82+
}),
83+
)
84+
}
13585

136-
const disposeOnTextChanged = useCallback(() => {
137-
for (const subscription of onTextChangedSubscriptions.current) {
138-
subscription.dispose();
86+
if (textModels.original != undefined) {
87+
modelSubscriptions.push(
88+
textModels.original.onDidChangeContent(() => {
89+
didModelContentChange(textModels, wrapperConfig.editorAppConfig?.codeResources, onTextChanged)
90+
}),
91+
)
92+
}
93+
94+
didModelContentChange(textModels, wrapperConfig.editorAppConfig?.codeResources, onTextChanged)
95+
})
96+
97+
editorRef.current?.start(editorContainerEl).then(() => {
98+
if (editorRef.current) onLoad?.(editorRef.current)
99+
})
100+
} catch (e) {
101+
if (onError) {
102+
onError(e)
103+
} else {
104+
throw e
139105
}
140-
onTextChangedSubscriptions.current = [];
141-
}, []);
142-
143-
return (
144-
<div
145-
ref={containerRef}
146-
style={style}
147-
className={className}
148-
/>
149-
);
150-
};
106+
}
107+
})
108+
}, [wrapperConfig, className, onError, onLoad, onTextChanged])
109+
110+
// this `useEffect` is used to dispose the modelSubscriptions when the component unmounts
111+
useEffect(() => {
112+
return () => {
113+
modelSubscriptions.forEach((model) => {
114+
model.dispose()
115+
})
116+
}
117+
}, [])
118+
119+
return <div ref={containerRef} style={style} className={className} />
120+
}

0 commit comments

Comments
 (0)