Реалізація Code Editor (Monaco/CodeMirror) на сайті
Вбудований редактор коду потрібен в IDE-інструментах, навчальних платформах, конфігураторах, playground-сторінках та будь-яких SaaS де користувач пише скрипти або конфігурації. Підключити <textarea> — не варіант: нема підсвітки синтаксису, нема автодоповнення, нема нормальної роботи з відступами.
Monaco Editor vs CodeMirror 6
Monaco Editor — це VS Code у браузері. Повне автодоповнення TypeScript з type-checking, go-to-definition, find-all-references. Важить ~7 МБ в бандлі. Виправдан для серйозних IDE-like інтерфейсів.
CodeMirror 6 — модульний, важить від 50 КБ (лише те, що підключено). Швидше ініціалізується, краще працює на мобільних. Для більшості use-case — правильний вибір.
Інтеграція Monaco в React
npm install @monaco-editor/react
Пакет завантажує Monaco через CDN з web worker, не розбухує бандл:
import Editor, { OnMount, BeforeMount } from '@monaco-editor/react'
import * as monaco from 'monaco-editor'
interface CodeEditorProps {
value: string
onChange: (value: string) => void
language?: string
readOnly?: boolean
}
export function CodeEditor({ value, onChange, language = 'typescript', readOnly = false }: CodeEditorProps) {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
const handleBeforeMount: BeforeMount = (monacoInstance) => {
// Реєструємо кастомну мову або тему до монтування
monacoInstance.editor.defineTheme('my-dark', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#0f172a',
'editor.lineHighlightBackground': '#1e293b',
},
})
}
const handleMount: OnMount = (editor, monacoInstance) => {
editorRef.current = editor
// TypeScript/JavaScript — налаштування compiler options
monacoInstance.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monacoInstance.languages.typescript.ScriptTarget.ES2020,
moduleResolution: monacoInstance.languages.typescript.ModuleResolutionKind.NodeJs,
strict: true,
})
// Додаємо type definitions для автодоповнення
monacoInstance.languages.typescript.typescriptDefaults.addExtraLib(
`declare module 'my-api' { export function query(sql: string): Promise<any[]> }`,
'file:///node_modules/my-api/index.d.ts'
)
// Гарячі клавіші
editor.addCommand(monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyCode.KeyS, () => {
onSave?.(editor.getValue())
})
}
return (
<Editor
height="400px"
language={language}
value={value}
theme="my-dark"
beforeMount={handleBeforeMount}
onMount={handleMount}
onChange={(val) => onChange(val ?? '')}
options={{
readOnly,
minimap: { enabled: false },
fontSize: 14,
tabSize: 2,
wordWrap: 'on',
scrollBeyondLastLine: false,
renderLineHighlight: 'line',
padding: { top: 16, bottom: 16 },
}}
/>
)
}
Кілька файлів (multi-model)
Коли потрібно переключатися між файлами без втрати cursor position та undo-history:
function MultiFileEditor({ files }: { files: File[] }) {
const [activeFile, setActiveFile] = useState(files[0].path)
const modelsRef = useRef<Record<string, monaco.editor.ITextModel>>({})
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
const handleMount: OnMount = (editor, monacoInstance) => {
editorRef.current = editor
// Створюємо model для кожного файлу
files.forEach((file) => {
const uri = monacoInstance.Uri.parse(`file:///${file.path}`)
modelsRef.current[file.path] = monacoInstance.editor.createModel(
file.content,
detectLanguage(file.path),
uri
)
})
editor.setModel(modelsRef.current[activeFile])
}
function switchFile(path: string) {
setActiveFile(path)
editorRef.current?.setModel(modelsRef.current[path])
}
return (
<div>
<div className="flex gap-1 border-b">
{files.map((f) => (
<button
key={f.path}
onClick={() => switchFile(f.path)}
className={activeFile === f.path ? 'bg-gray-800 text-white' : ''}
>
{f.name}
</button>
))}
</div>
<Editor onMount={handleMount} /* ... */ />
</div>
)
}
CodeMirror 6: більш легка альтернатива
npm install @codemirror/view @codemirror/state @codemirror/lang-javascript \
@codemirror/lang-python @codemirror/lang-css \
@codemirror/theme-one-dark @codemirror/basic-setup
import { useEffect, useRef } from 'react'
import { EditorView, basicSetup } from 'codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { oneDark } from '@codemirror/theme-one-dark'
import { EditorState } from '@codemirror/state'
function CodeMirrorEditor({ value, onChange }: { value: string; onChange: (v: string) => void }) {
const containerRef = useRef<HTMLDivElement>(null)
const viewRef = useRef<EditorView | null>(null)
useEffect(() => {
if (!containerRef.current) return
const updateListener = EditorView.updateListener.of((update) => {
if (update.docChanged) {
onChange(update.state.doc.toString())
}
})
const view = new EditorView({
state: EditorState.create({
doc: value,
extensions: [
basicSetup,
javascript({ typescript: true }),
oneDark,
updateListener,
EditorView.lineWrapping,
],
}),
parent: containerRef.current,
})
viewRef.current = view
return () => view.destroy()
}, []) // Монтуємо один раз
// Обновляємо значення ззовні без пересоздання редактора
useEffect(() => {
const view = viewRef.current
if (!view) return
const current = view.state.doc.toString()
if (current !== value) {
view.dispatch({
changes: { from: 0, to: current.length, insert: value },
})
}
}, [value])
return <div ref={containerRef} className="border rounded overflow-hidden" />
}
Що робимо
Вибираємо редактор під задачу (Monaco для IDE-like, CodeMirror для компактних сценаріїв), налаштовуємо мови, тему під дизайн проекту, гарячі клавіші, валідацію. При необхідності підключаємо language server по WebSocket для повноцінного IntelliSense на бекенді.
Термін: базовий редактор з підсвіткою та автодоповненням — 1–2 дні. Мультифайловий редактор з LSP — 3–4 дні.







