@monaco-editor/react
`@monaco-editor/react` is a React wrapper and loader for Monaco Editor, the browser code editor that powers VS Code. It mounts standard and diff editors, exposes both Monaco and editor instances through hooks and callbacks, manages models by path for tabbed interfaces, and hides most AMD loader configuration. The wrapper itself is small, but the editor it loads is a large browser application with workers, language services, models, commands, and its own lifecycle.
Use it when the product genuinely needs Monaco's IDE behavior, especially diff views or multi-file models. For forms, snippets, and modest configuration editors, CodeMirror is usually lighter and easier to own.
Use it if
- You need a VS Code-like editor or diff view inside a React application
- You want Monaco without ejecting or hand-configuring a typical Vite, Next.js, or generated React build
- You need multi-file models that preserve undo stacks, selections, scroll positions, and view state by path
- You need direct access to Monaco language services, markers, themes, commands, and editor instances
- You only need a textarea with syntax color: Monaco's workers, models, language services, and editor runtime are far heavier than CodeMirror or a highlighted textarea, even though this wrapper is about 4.8 KB gzipped
- Your application must work under a strict offline or Content Security Policy setup with no extra asset work: the README says Monaco files load from a CDN by default, so you must self-host and configure `loader` before initialization
- You expect rich validation for every displayed language: the README lists rich IntelliSense and validation for TypeScript, JavaScript, CSS-family languages, JSON, and HTML, while many others only receive basic syntax coloring
- You render the editor during server-side rendering: Monaco depends on browser globals, and the README's Next.js note warns that source files use `document`; isolate it in a client-only component or dynamic import
- You need a simple controlled React input with no imperative lifecycle: Monaco initializes asynchronously, `useMonaco()` initially returns null, and models plus editor instances can outlive component renders unless disposal and `keepCurrentModel` behavior are deliberate
Setup reality
Install `@monaco-editor/react` with React and React DOM. Version 4.7.0 declares React 16.8 through 19 and `monaco-editor >=0.25 <1` as peers; the README notes that TypeScript definitions depend on `monaco-editor`, so install it explicitly even when the default CDN supplies runtime files. Out of the box, the loader downloads Monaco's AMD assets from a CDN at first mount. That means the first editor can show a loading state, offline use fails, and CSP, Electron, air-gapped, or privacy-sensitive deployments need `loader.config(...)` before any editor or `useMonaco()` call initializes the singleton. Self-hosting with the npm Monaco package can require worker and bundler configuration, and the README warns that some generated setups may need plugins or ejection. Next.js must mount the editor on the client because Monaco touches `document`. `useMonaco()` returns null on its first render because initialization is asynchronous. Choose controlled `value` versus one-time `defaultValue` intentionally. For multi-file editing, stable URI-like `path` values identify models; default props only apply when a model is created, and retained models consume memory until disposed. `onValidate` is not a universal linter, and large files or too many live models need editor options and lifecycle limits. Keep secrets out of editor text and callbacks because models are in browser memory.
Patterns
Render a basic code editorrender-editor
import Editor from '@monaco-editor/react';
export function CodeField() {
return (
<Editor
height="60vh"
defaultLanguage="typescript"
defaultValue="const answer: number = 42;"
/>
);
}`defaultValue` and `defaultLanguage` apply when the model is created; use controlled props for later external updates.
Use a controlled editor valuecontrol-editor-value
const [code, setCode] = useState('');
<Editor
language="json"
value={code}
onChange={(value) => setCode(value ?? '')}
/>;`onChange` can receive `undefined`; normalize it before storing a string state.
Keep the editor instance in a refaccess-editor-instance
const editorRef = useRef(null);
<Editor onMount={(editor) => { editorRef.current = editor; }} />;
function formatDocument() {
editorRef.current?.getAction('editor.action.formatDocument')?.run();
}Do not put the editor instance in React state; it is mutable and should not trigger rendering.
Configure TypeScript before mountconfigure-typescript
<Editor
defaultLanguage="typescript"
beforeMount={(monaco) => {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
strict: true,
target: monaco.languages.typescript.ScriptTarget.ES2022,
});
}}
/>;Use `beforeMount` for language-service defaults so they are ready before the first model is shown.
Use Monaco outside the editor callbackuse-monaco-hook
const monaco = useMonaco();
useEffect(() => {
if (!monaco) return;
monaco.editor.defineTheme('app-dark', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {},
});
}, [monaco]);The hook initially returns null because the shared loader initializes asynchronously.
Use the installed Monaco package instead of the CDNself-host-monaco
import * as monaco from 'monaco-editor';
import { loader } from '@monaco-editor/react';
loader.config({ monaco });Run this before any editor or hook initializes; your bundler must also package Monaco workers and assets correctly.
Compare two documentsrender-diff-editor
import { DiffEditor } from '@monaco-editor/react';
<DiffEditor
height="70vh"
language="json"
original={beforeJson}
modified={afterJson}
options={{ readOnly: true, renderSideBySide: true }}
/>;Provide stable model paths when the diff documents need persistent model identity across renders.
Switch models by file pathbuild-multi-file-editor
<Editor
path={`file:///${activeFile.name}`}
defaultLanguage={activeFile.language}
defaultValue={activeFile.contents}
saveViewState
/>;A stable `path` preserves each model's undo stack, selection, and scroll state; default props are ignored when that model already exists.
Read language-service validation markersobserve-validation-markers
<Editor
defaultLanguage="json"
onValidate={(markers) => {
setErrors(markers.map((m) => ({
message: m.message,
line: m.startLineNumber,
})));
}}
/>;The README says this fires with rich validation languages, not every language that Monaco can colorize.
Tune an embedded read-only viewerconfigure-editor-options
<Editor
value={code}
language="javascript"
options={{
readOnly: true,
minimap: { enabled: false },
lineNumbers: 'off',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>;`automaticLayout` is convenient in responsive containers but adds resize observation work.
Register a custom completion provideradd-completion-provider
function beforeMount(monaco) {
monaco.languages.registerCompletionItemProvider('yaml', {
provideCompletionItems: () => ({
suggestions: [{
label: 'service',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'service: ${1:name}',
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
}],
}),
});
}
<Editor beforeMount={beforeMount} defaultLanguage="yaml" />;Registration returns a disposable; keep and dispose it if the provider is scoped to a component lifetime.
Keep a model across component unmountspreserve-model-on-unmount
<Editor
path="file:///draft.ts"
keepCurrentModel
saveViewState
/>;Retained models consume memory; dispose them explicitly with Monaco when the document is truly closed.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @uiw/react-codemirror | npm | You want a lighter React editor assembled from CodeMirror 6 extensions |
| react-ace | npm | You already use Ace modes and themes and need a mature React wrapper around them |
| @codemirror/view | npm | You can build your own React integration and want direct control over a modular editor core |