Skip to content

Commit 0021e1d

Browse files
committed
fix(editors): scope Monaco editor instance and add shallow equality to Zustand selectors (#782)
Bug 1 - Race Condition Fix: - Replace getModels()?.[0] with scoped useRef in ConcertoEditor.tsx - Error markers now correctly attach to the Concerto editor only Bug 2 - Performance Fix: - Add shallow equality from zustand/shallow to all object-returning Zustand selectors across 9 components - Prevents unnecessary re-renders on every keystroke Files modified: - src/editors/ConcertoEditor.tsx (both fixes) - src/App.tsx - src/editors/JSONEditor.tsx - src/editors/MarkdownEditor.tsx - src/components/PlaygroundSidebar.tsx - src/components/ProblemPanel.tsx - src/components/AIConfigPopup.tsx - src/components/AIChatPanel.tsx - src/components/CodeSelectionMenu.tsx Signed-off-by: Rupesh <rupeshkuneed@gmail.com>
1 parent 3031771 commit 0021e1d

9 files changed

Lines changed: 25 additions & 12 deletions

File tree

src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Routes, Route, useSearchParams, useNavigate } from "react-router-dom";
55
import Navbar from "./components/Navbar";
66
import tour from "./components/Tour";
77
import useAppStore from "./store/store";
8+
import { shallow } from "zustand/shallow";
89
import LearnContent from "./components/Content";
910
import PlaygroundSidebar from "./components/PlaygroundSidebar";
1011
import "./styles/App.css";
@@ -23,7 +24,7 @@ const App = () => {
2324
useAppStore((state) => ({
2425
isAIConfigOpen: state.isAIConfigOpen,
2526
setAIConfigOpen: state.setAIConfigOpen,
26-
}));
27+
}), shallow);
2728
const backgroundColor = useAppStore((state) => state.backgroundColor);
2829
const textColor = useAppStore((state) => state.textColor);
2930
const [loading, setLoading] = useState(true);

src/components/AIChatPanel.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useRef, useState, useMemo } from "react";
22
import ReactMarkdown from "react-markdown";
33
import useAppStore from "../store/store";
4+
import { shallow } from "zustand/shallow";
45
import { sendMessage, stopMessage } from "../ai-assistant/chatRelay";
56

67
export const AIChatPanel = () => {
@@ -13,7 +14,7 @@ export const AIChatPanel = () => {
1314
editorTemplateMark: state.editorValue,
1415
editorModelCto: state.editorModelCto,
1516
editorAgreementData: state.editorAgreementData,
16-
}));
17+
}), shallow);
1718

1819
const { chatState, resetChat, aiConfig, setAIConfig, setAIConfigOpen, setAIChatOpen, textColor, backgroundColor } = useAppStore((state) => ({
1920
chatState: state.chatState,
@@ -24,7 +25,7 @@ export const AIChatPanel = () => {
2425
setAIChatOpen: state.setAIChatOpen,
2526
textColor: state.textColor,
2627
backgroundColor: state.backgroundColor
27-
}));
28+
}), shallow);
2829

2930
const latestMessageRef = useRef<HTMLDivElement>(null);
3031

src/components/AIConfigPopup.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react';
22
import { AIConfig, AIConfigPopupProps, KeyProtectionLevel } from '../types/components/AIAssistant.types';
33
import { useDebounce } from 'use-debounce';
44
import useAppStore from '../store/store';
5+
import { shallow } from 'zustand/shallow';
56
import {
67
isWebAuthnPRFSupported,
78
encryptAndStoreApiKey,
@@ -15,7 +16,7 @@ const AIConfigPopup = ({ isOpen, onClose }: AIConfigPopupProps) => {
1516
backgroundColor: state.backgroundColor,
1617
keyProtectionLevel: state.keyProtectionLevel,
1718
setKeyProtectionLevel: state.setKeyProtectionLevel,
18-
}));
19+
}), shallow);
1920

2021
const theme = useMemo(() => {
2122
const isDarkMode = backgroundColor !== '#ffffff';

src/components/CodeSelectionMenu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
22
import { sendMessage } from '../ai-assistant/chatRelay';
33
import { CodeSelectionMenuProps } from '../types/components/AIAssistant.types';
44
import useAppStore from '../store/store';
5+
import { shallow } from 'zustand/shallow';
56
import ReactMarkdown from "react-markdown";
67
import * as monaco from 'monaco-editor';
78

@@ -48,7 +49,7 @@ const CodeSelectionMenu: React.FC<CodeSelectionMenuProps> = ({
4849
editorTemplateMark: state.editorValue,
4950
editorModelCto: state.editorModelCto,
5051
editorAgreementData: state.editorAgreementData,
51-
}));
52+
}), shallow);
5253

5354
const handleExplain = async () => {
5455
if (!aiConfig) {

src/components/PlaygroundSidebar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FiTerminal, FiShare2, FiSettings } from "react-icons/fi";
55
import { FaCirclePlay } from "react-icons/fa6";
66
import { IoChatbubbleEllipsesOutline } from "react-icons/io5";
77
import useAppStore from "../store/store";
8+
import { shallow } from "zustand/shallow";
89
import { message, Tooltip } from "antd";
910
import FullScreenModal from "./FullScreenModal";
1011
import SettingsModal from "./SettingsModal";
@@ -34,7 +35,7 @@ const PlaygroundSidebar = () => {
3435
setAIChatOpen: state.setAIChatOpen,
3536
generateShareableLink: state.generateShareableLink,
3637
setSettingsOpen: state.setSettingsOpen,
37-
}));
38+
}), shallow);
3839

3940
const handleShare = async () => {
4041
try {

src/components/ProblemPanel.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useMemo, useCallback } from 'react';
22
import useAppStore from '../store/store';
3+
import { shallow } from 'zustand/shallow';
34
import { navigateToLine, EditorSource } from '../utils/editorNavigation';
45
import '../styles/components/ProblemPanel.css';
56

@@ -18,7 +19,7 @@ const ProblemPanel: React.FC = () => {
1819
error: state.error,
1920
backgroundColor: state.backgroundColor,
2021
textColor: state.textColor
21-
}));
22+
}), shallow);
2223

2324
const parseError = (errorMessage: string) => {
2425
const errors: Omit<ProblemItem, 'id' | 'timestamp'>[] = [];

src/editors/ConcertoEditor.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useMonaco } from "@monaco-editor/react";
2-
import { lazy, Suspense, useCallback, useEffect, useMemo, MutableRefObject } from "react";
2+
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, MutableRefObject } from "react";
33
import * as monaco from "monaco-editor";
44
import useAppStore from "../store/store";
5+
import { shallow } from "zustand/shallow";
56
import { useCodeSelection } from "../components/CodeSelectionMenu";
67
import { registerAutocompletion } from "../ai-assistant/autocompletion";
78
import { registerEditor, unregisterEditor } from "../utils/editorNavigation";
@@ -124,12 +125,13 @@ export default function ConcertoEditor({
124125
}: ConcertoEditorProps) {
125126
const { handleSelection, MenuComponent } = useCodeSelection("concerto");
126127
const monacoInstance = useMonaco();
128+
const localEditorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
127129
const { error, backgroundColor, aiConfig, showLineNumbers } = useAppStore((state) => ({
128130
error: state.error,
129131
backgroundColor: state.backgroundColor,
130132
aiConfig: state.aiConfig,
131133
showLineNumbers: state.showLineNumbers,
132-
}));
134+
}), shallow);
133135
const ctoErr = error?.startsWith("c:") ? error : undefined;
134136

135137
const themeName = useMemo(
@@ -165,6 +167,7 @@ export default function ConcertoEditor({
165167
}), [aiConfig?.enableInlineSuggestions, showLineNumbers]);
166168

167169
const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
170+
localEditorRef.current = editor;
168171
if (editorRef) {
169172
editorRef.current = editor;
170173
}
@@ -190,7 +193,9 @@ export default function ConcertoEditor({
190193
useEffect(() => {
191194
if (!monacoInstance) return;
192195

193-
const model = monacoInstance.editor.getModels()?.[0];
196+
const editor = localEditorRef.current;
197+
if (!editor) return;
198+
const model = editor.getModel();
194199
if (!model) return;
195200

196201
if (ctoErr) {

src/editors/JSONEditor.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { lazy, Suspense, useMemo, useCallback, useEffect } from "react";
22
import * as monaco from "monaco-editor";
33
import useAppStore from "../store/store";
4+
import { shallow } from "zustand/shallow";
45
import { useCodeSelection } from "../components/CodeSelectionMenu";
56
import { registerAutocompletion } from "../ai-assistant/autocompletion";
67
import { registerEditor, unregisterEditor } from "../utils/editorNavigation";
@@ -24,7 +25,7 @@ export default function JSONEditor({
2425
backgroundColor: state.backgroundColor,
2526
aiConfig: state.aiConfig,
2627
showLineNumbers: state.showLineNumbers,
27-
}));
28+
}), shallow);
2829

2930
const themeName = useMemo(
3031
() => (backgroundColor ? "darkTheme" : "lightTheme"),

src/editors/MarkdownEditor.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { lazy, Suspense, useMemo, useCallback, useEffect, MutableRefObject } from "react";
22
import useAppStore from "../store/store";
3+
import { shallow } from "zustand/shallow";
34
import { useMonaco } from "@monaco-editor/react";
45
import { useCodeSelection } from "../components/CodeSelectionMenu";
56
import type { editor } from "monaco-editor";
@@ -28,7 +29,7 @@ export default function MarkdownEditor({
2829
textColor: state.textColor,
2930
aiConfig: state.aiConfig,
3031
showLineNumbers: state.showLineNumbers,
31-
}));
32+
}), shallow);
3233
const monaco = useMonaco();
3334

3435
const themeName = useMemo(

0 commit comments

Comments
 (0)