Skip to content

Commit 6013d4e

Browse files
committed
Use useEffect to perform initialisation
As recommended by yjs's source code: https://github.com/yjs/yjs-demos/blob/main/monaco-react
1 parent c230c92 commit 6013d4e

File tree

2 files changed

+77
-98
lines changed

2 files changed

+77
-98
lines changed

frontend/src/domain/context/CollaborationContext.tsx

Lines changed: 72 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { createContext, useState, useContext, ReactNode, useEffect, useRef } from "react";
1+
import React, { createContext, useState, useContext, ReactNode, useEffect, useRef, useMemo } from "react";
22
import { Monaco } from "@monaco-editor/react";
33
import * as Y from "yjs";
44
import * as monaco from "monaco-editor";
@@ -12,13 +12,14 @@ import { Language } from "domain/entities/Language";
1212
import { CodeExecResult } from "domain/entities/CodeExecResult";
1313

1414
interface CollaborationContextType {
15-
initialiseEditor: (roomId: string, editor: any, monaco: Monaco) => void;
15+
onEditorIsMounted: (editor: monaco.editor.IStandaloneCodeEditor) => void;
1616
selectedLanguage: Language;
1717
languages: Language[];
1818
handleChangeLanguage: (lang: Language) => void;
1919
handleExecuteCode: () => Promise<void>;
2020
isExecuting: boolean;
2121
execResult: CodeExecResult | null;
22+
setRoomId: (roomId: string) => void;
2223
}
2324

2425
const CollaborationContext = createContext<CollaborationContextType | undefined>(undefined);
@@ -36,126 +37,98 @@ export const CollaborationProvider: React.FC<{ children: ReactNode }> = ({ child
3637
alias: "Javascript"
3738
});
3839
const [languages, setLanguages] = useState<Language[]>([]);
39-
4040
const [execResult, setExecResult] = useState<CodeExecResult | null>(null);
41-
4241
const [isExecuting, setIsExecuting] = useState<boolean>(false);
4342

44-
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
45-
const monacoRef = useRef<Monaco | null>(null);
46-
const bindingRef = useRef<MonacoBinding | null>(null);
47-
const providerRef = useRef<WebsocketProvider | null>(null);
48-
const ydocRef = useRef<Y.Doc | null>(null);
49-
const yMapRef = useRef<Y.Map<any> | null>(null);
50-
51-
const initialiseEditor = async (roomId: string, editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => {
52-
editorRef.current = editor;
53-
monacoRef.current = monaco;
54-
55-
const { yDoc, provider, yMap } = initialiseYdoc(roomId);
43+
const ydoc = useMemo(() => new Y.Doc(), []);
44+
const ymap: Y.Map<any> = useMemo(() => ydoc.getMap("sharedMap"), [ydoc]);
5645

57-
bindEditorToDoc(editor, yDoc, provider);
58-
setUpObserver(yMap);
59-
setUpConnectionAwareness(provider);
60-
await initialiseLanguages(monaco, yMap, editor);
61-
};
62-
63-
const initialiseLanguages = async (
64-
monaco: Monaco,
65-
yMap: Y.Map<any>,
66-
editor: monaco.editor.IStandaloneCodeEditor
67-
) => {
68-
// Initialise language dropdown
69-
const allLanguages = monaco.languages.getLanguages();
70-
const pistonLanguageVersions = await PistonClient.getLanguageVersions();
71-
setLanguages(
72-
allLanguages
73-
.filter((lang) => pistonLanguageVersions.some((pistonLang: any) => pistonLang.language === lang.id))
74-
.map((lang) => ({
75-
alias: lang.aliases && lang.aliases.length > 0 ? lang.aliases[0] : lang.id,
76-
language: lang.id,
77-
version: pistonLanguageVersions.find((pistonLang: any) => pistonLang.language === lang.id)?.version
78-
}))
79-
);
46+
const [roomId, setRoomId] = useState<string | null>(null);
47+
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
48+
const [provider, setProvider] = useState<WebsocketProvider | null>(null);
49+
const [binding, setBinding] = useState<MonacoBinding | null>(null);
8050

81-
// Set the editor's language
82-
const language: Language = yMap.get(SELECTED_LANGUAGE);
83-
const model = editor?.getModel();
84-
if (model) {
85-
monaco.editor.setModelLanguage(model, language?.language ?? "javascript");
51+
// This effect manages the lifetime of the yjs doc and the provider
52+
useEffect(() => {
53+
if (roomId == null) {
54+
return;
8655
}
87-
};
88-
89-
const initialiseYdoc = (roomId: string): { yDoc: Y.Doc; yMap: Y.Map<any>; provider: WebsocketProvider } => {
90-
const yDoc = new Y.Doc();
91-
const yMap: Y.Map<any> = yDoc.getMap("sharedMap");
92-
ydocRef.current = yDoc;
93-
yMapRef.current = yMap;
94-
// TODO: Replace serverUrl once BE ready
95-
// Test locally across browers with 'HOST=localhost PORT 1234 npx y-websocket'
96-
const provider = new WebsocketProvider("ws://localhost:1234", roomId, yDoc);
97-
provider.on("status", (event: any) => {
98-
if (event.status === "disconnected") {
99-
//toast.error("You have disconnected");
100-
}
56+
const provider = new WebsocketProvider("ws://localhost:1234", roomId, ydoc);
57+
setProvider(provider);
58+
provider.awareness.setLocalStateField(USERNAME, username);
59+
provider.awareness.on("change", (update: any) => {
60+
const users = provider.awareness.getStates();
61+
// TODO: Some UI feedback about connection status of the other user
10162
});
102-
providerRef.current = provider;
103-
return { yDoc, yMap, provider };
104-
};
63+
return () => {
64+
provider?.destroy();
65+
ydoc?.destroy();
66+
};
67+
}, [ydoc, roomId]);
10568

106-
const bindEditorToDoc = (editor: monaco.editor.IStandaloneCodeEditor, yDoc: Y.Doc, provider: WebsocketProvider) => {
107-
const type = yDoc.getText("monaco");
108-
const editorModel = editor.getModel();
109-
if (editorModel == null) {
110-
toast.error("There was an issue with initialising the code editor");
69+
// This effect manages the lifetime of the editor binding
70+
useEffect(() => {
71+
if (provider == null || editor == null || editor.getModel() == null) {
11172
return;
11273
}
113-
const binding = new MonacoBinding(type, editorModel, new Set([editor]), provider.awareness);
114-
bindingRef.current = binding;
115-
};
11674

117-
// Observer to listen to any changes to shared state (e.g. language changes)
118-
const setUpObserver = (yMap: Y.Map<any>) => {
119-
yMap.observe((event) => {
75+
const binding = new MonacoBinding(
76+
ydoc.getText("monaco"),
77+
editor.getModel()!,
78+
new Set([editor]),
79+
provider?.awareness
80+
);
81+
82+
setBinding(binding);
83+
84+
ymap.observe((event) => {
12085
event.changes.keys.forEach((change, key) => {
12186
if (key === SELECTED_LANGUAGE) {
122-
const language: Language = yMap.get(SELECTED_LANGUAGE);
87+
const language: Language = ymap.get(SELECTED_LANGUAGE);
12388
setSelectedLanguage(language);
124-
const model = editorRef.current?.getModel();
125-
if (model) {
126-
monaco.editor.setModelLanguage(model, language.language);
127-
}
89+
const model = editor.getModel();
90+
monaco.editor.setModelLanguage(model!, language.language);
12891
}
12992
});
13093
});
131-
};
13294

133-
// Observer to listen to any changes to users' presence (e.g. connection status, is typing, cursor)
134-
const setUpConnectionAwareness = (provider: WebsocketProvider) => {
135-
provider.awareness.setLocalStateField(USERNAME, username);
136-
provider.awareness.on("change", (update: any) => {
137-
const users = provider.awareness.getStates();
138-
// TODO: Some UI feedback about connection status of the other user
139-
});
140-
};
95+
// Set the editor's language
96+
const language: Language = ymap.get(SELECTED_LANGUAGE);
97+
const model = editor.getModel();
98+
monaco.editor.setModelLanguage(model!, language?.language ?? "javascript");
14199

142-
useEffect(() => {
143100
return () => {
144-
bindingRef.current?.destroy();
145-
providerRef.current?.disconnect();
146-
editorRef.current?.dispose();
147-
ydocRef.current?.destroy();
101+
binding.destroy();
148102
};
103+
}, [ydoc, provider, editor, ymap]);
104+
105+
useEffect(() => {
106+
initialiseLanguages();
149107
}, []);
150108

109+
const initialiseLanguages = async () => {
110+
// Initialise language dropdown
111+
const allLanguages = monaco.languages.getLanguages();
112+
const pistonLanguageVersions = await PistonClient.getLanguageVersions();
113+
setLanguages(
114+
allLanguages
115+
.filter((lang) => pistonLanguageVersions.some((pistonLang: any) => pistonLang.language === lang.id))
116+
.map((lang) => ({
117+
alias: lang.aliases && lang.aliases.length > 0 ? lang.aliases[0] : lang.id,
118+
language: lang.id,
119+
version: pistonLanguageVersions.find((pistonLang: any) => pistonLang.language === lang.id)?.version
120+
}))
121+
);
122+
};
123+
151124
const handleChangeLanguage = (lang: Language) => {
152-
yMapRef.current?.set(SELECTED_LANGUAGE, lang);
125+
ymap.set(SELECTED_LANGUAGE, lang);
153126
};
154127

155128
const handleExecuteCode = async () => {
156129
try {
157130
setIsExecuting(true);
158-
const sourceCode = editorRef.current?.getValue();
131+
const sourceCode = editor?.getValue();
159132
if (!sourceCode) {
160133
// TODO
161134
return;
@@ -169,10 +142,15 @@ export const CollaborationProvider: React.FC<{ children: ReactNode }> = ({ child
169142
}
170143
};
171144

145+
const onEditorIsMounted = (editor: monaco.editor.IStandaloneCodeEditor) => {
146+
setEditor(editor);
147+
};
148+
172149
return (
173150
<CollaborationContext.Provider
174151
value={{
175-
initialiseEditor,
152+
onEditorIsMounted,
153+
setRoomId,
176154
selectedLanguage,
177155
languages,
178156
handleChangeLanguage,

frontend/src/presentation/components/CodeEditor/CodeEditor.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import styles from "./CodeEditor.module.css";
33
import Editor, { Monaco } from "@monaco-editor/react";
44
import { Button, Spin } from "antd";
55
import { useCollaboration } from "domain/context/CollaborationContext";
6-
import { PlayCircleOutlined, CloudUploadOutlined } from "@ant-design/icons";
76
import * as monaco from "monaco-editor";
87
import { SunOutlined, MoonFilled } from "@ant-design/icons";
98
import { LanguageSelector } from "./LanguageSelector";
@@ -14,10 +13,12 @@ interface CodeEditorProps {
1413
}
1514

1615
const CodeEditor: React.FC<CodeEditorProps> = ({ roomId }) => {
17-
const { initialiseEditor, isExecuting } = useCollaboration();
16+
const { onEditorIsMounted, isExecuting, setRoomId } = useCollaboration();
1817
const [theme, setTheme] = useState("vs-light");
19-
const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => {
20-
initialiseEditor(roomId, editor, monaco);
18+
19+
const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
20+
onEditorIsMounted(editor);
21+
setRoomId(roomId);
2122
};
2223

2324
const handleToggleTheme = () => {

0 commit comments

Comments
 (0)