Skip to content

Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.

The Problem

CodeMirror mounts but shows nothing:

import { EditorView, basicSetup } from 'codemirror';

const view = new EditorView({
  parent: document.getElementById('editor')!,
});
// Empty editor — no gutters, no highlighting

Or the React wrapper doesn’t sync with state:

const [code, setCode] = useState('hello');
// Editor shows 'hello' but typing doesn't update `code`

Or language highlighting doesn’t apply:

JavaScript code renders as plain text — no colors

Why This Happens

CodeMirror 6 is a modular editor — it ships with almost nothing by default. Every feature is an extension, and the boundary between the editor core and language/theme packages is sharper than in any other browser editor. That design pays off for bundle size, but it also means that copy-pasting a CodeMirror 5 example into a v6 project produces a blank <div> and no error.

The second source of confusion is React. CodeMirror manages its own document state in a Transaction system that is not React’s, so trying to treat the editor as a controlled input (value={code} driven by setCode) creates an infinite loop or a frozen cursor. The supported pattern is uncontrolled with explicit sync, via EditorView.updateListener and Compartment.reconfigure.

Common causes:

  • basicSetup provides essential features — without it, you get a plain textarea with no line numbers, no syntax highlighting, and no keyboard shortcuts. basicSetup bundles line numbers, undo/redo, bracket matching, and other fundamentals.
  • Languages are separate packages@codemirror/lang-javascript, @codemirror/lang-python, etc. must be installed and added to the extensions array. Without a language extension, code is plain text.
  • CodeMirror manages its own state — unlike a controlled React input, CodeMirror maintains internal state. You must use EditorView.updateListener or a dispatch handler to sync with React state. Directly setting value from React does not work without reconciliation.
  • Themes are extensions, not CSS — CodeMirror 6 uses its own styling system. A theme is an extension that defines editor colors. Without a theme extension, the editor uses minimal browser defaults.
  • CM5 vs CM6 packages coexist on npm[email protected] (legacy, single package) and [email protected] (the meta-package over @codemirror/* modules) ship side by side. Installing codemirror without a version pin can land you on whichever the registry currently labels latest.

Version History That Changes the Failure Mode

CodeMirror 6 was a complete rewrite, and the editor you are using depends entirely on which line you installed.

  • CodeMirror 5 (2013–present, maintenance mode) — single-package CommonJS bundle. Imported as import CodeMirror from 'codemirror' and configured by passing a giant options object. Language modes live under codemirror/mode/* and are loaded at runtime by adding <script> tags or require(). Theming is pure CSS. Stable, large bundle, still used by older Jupyter notebooks and Bitbucket.
  • CodeMirror 6 (June 2021) — ESM-only rewrite. The editor is split into roughly twenty packages under @codemirror/* (view, state, language, commands, autocomplete, lint, search, theme-one-dark, lang-javascript, lang-python, etc.). State is immutable and updated via transactions. Extensions are the only configuration surface. CM5 examples will not run.
  • CodeMirror 6.5 (mid 2023) — added hover tooltips (hoverTooltip) and improved input method (IME) handling on Chinese/Japanese keyboards. If you are stuck on a 6.0–6.4 release, tooltip APIs you find in tutorials may not exist.
  • CodeMirror 6.6 (late 2023) — better RTL support, performance fixes for very long lines, and improved gutter customization. Custom gutter code from 6.0 may need tweaks.
  • CodeMirror 6.0.x of @codemirror/lang-* — the language packages have their own semver. @codemirror/lang-javascript v6.2+ added TypeScript-in-JS support via the typescript: true option. Earlier versions ignored the flag. Mismatches between codemirror (the meta-package) and individual @codemirror/lang-* versions are silent — the editor just falls back to plain text.
  • @uiw/react-codemirror (community React wrapper) — tracks CM6 closely. v4.x targets CM6.0–6.2, v4.20+ targets CM6.3+. If your theme prop is ignored, you may be on an old wrapper version that pre-dates the theme prop.
  • Ecosystem comparison — Monaco Editor (VS Code’s editor) is heavier and uses Web Workers for language services. Ace Editor is the old standard, still actively maintained but architecturally similar to CM5. CodeMirror 6 is the smallest of the three, at the cost of demanding more wiring.

If npm install codemirror lands you on v5 and your tutorial assumes v6, the editor mounts with the wrong API surface entirely. Pin "codemirror": "^6.0.0" explicitly.

Fix 1: Vanilla JavaScript Setup

npm install codemirror @codemirror/lang-javascript @codemirror/lang-python @codemirror/lang-html @codemirror/lang-css @codemirror/lang-json
npm install @codemirror/theme-one-dark
import { EditorView, basicSetup } from 'codemirror';
import { EditorState } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';

const state = EditorState.create({
  doc: 'const greeting = "Hello, World!";\nconsole.log(greeting);',
  extensions: [
    basicSetup,                         // Line numbers, undo, brackets, etc.
    javascript({ typescript: true, jsx: true }),  // Language support
    oneDark,                            // Theme
    EditorView.lineWrapping,           // Word wrap
    EditorView.updateListener.of((update) => {
      if (update.docChanged) {
        const newCode = update.state.doc.toString();
        console.log('Code changed:', newCode);
      }
    }),
  ],
});

// Mount — parent element MUST exist and have dimensions
const view = new EditorView({
  state,
  parent: document.getElementById('editor')!,
});

// Update content programmatically
view.dispatch({
  changes: { from: 0, to: view.state.doc.length, insert: 'new content' },
});

// Cleanup
view.destroy();

Fix 2: React Integration

npm install @uiw/react-codemirror
# Or build your own wrapper (see Fix 3)
// Using @uiw/react-codemirror (simplest)
'use client';

import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { useState } from 'react';

function Editor() {
  const [code, setCode] = useState('const x: number = 42;');

  return (
    <CodeMirror
      value={code}
      height="400px"
      theme={oneDark}
      extensions={[javascript({ typescript: true, jsx: true })]}
      onChange={(value) => setCode(value)}
      basicSetup={{
        lineNumbers: true,
        highlightActiveLineGutter: true,
        foldGutter: true,
        autocompletion: true,
        bracketMatching: true,
        closeBrackets: true,
        indentOnInput: true,
      }}
    />
  );
}

Fix 3: Custom React Wrapper

// components/CodeEditor.tsx — full control
'use client';

import { useRef, useEffect, useState } from 'react';
import { EditorView, basicSetup } from 'codemirror';
import { EditorState, type Extension } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { json } from '@codemirror/lang-json';
import { oneDark } from '@codemirror/theme-one-dark';

const languageMap: Record<string, () => Extension> = {
  javascript: () => javascript({ typescript: false, jsx: true }),
  typescript: () => javascript({ typescript: true, jsx: true }),
  python: () => python(),
  html: () => html(),
  css: () => css(),
  json: () => json(),
};

interface CodeEditorProps {
  value: string;
  onChange?: (value: string) => void;
  language?: string;
  readOnly?: boolean;
  height?: string;
}

export function CodeEditor({
  value,
  onChange,
  language = 'typescript',
  readOnly = false,
  height = '400px',
}: CodeEditorProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const viewRef = useRef<EditorView | null>(null);
  const onChangeRef = useRef(onChange);
  onChangeRef.current = onChange;

  useEffect(() => {
    if (!containerRef.current) return;

    const langExtension = languageMap[language]?.() ?? javascript({ typescript: true });

    const state = EditorState.create({
      doc: value,
      extensions: [
        basicSetup,
        langExtension,
        oneDark,
        EditorView.lineWrapping,
        EditorState.readOnly.of(readOnly),
        EditorView.updateListener.of((update) => {
          if (update.docChanged) {
            onChangeRef.current?.(update.state.doc.toString());
          }
        }),
      ],
    });

    const view = new EditorView({ state, parent: containerRef.current });
    viewRef.current = view;

    return () => {
      view.destroy();
      viewRef.current = null;
    };
  }, [language, readOnly]);  // Recreate on language or readOnly change

  // Sync external value changes
  useEffect(() => {
    const view = viewRef.current;
    if (!view) return;

    const currentValue = view.state.doc.toString();
    if (currentValue !== value) {
      view.dispatch({
        changes: { from: 0, to: currentValue.length, insert: value },
      });
    }
  }, [value]);

  return <div ref={containerRef} style={{ height, overflow: 'auto' }} />;
}

Fix 4: Custom Extensions

import { keymap } from '@codemirror/view';
import { indentWithTab } from '@codemirror/commands';
import { EditorView } from 'codemirror';

// Tab indentation (not enabled by default)
const tabExtension = keymap.of([indentWithTab]);

// Custom keybindings
const customKeymap = keymap.of([
  {
    key: 'Mod-s',
    run: (view) => {
      const code = view.state.doc.toString();
      handleSave(code);
      return true;
    },
  },
  {
    key: 'Mod-Enter',
    run: (view) => {
      const code = view.state.doc.toString();
      handleRun(code);
      return true;
    },
  },
]);

// Read-only mode
import { EditorState } from '@codemirror/state';
const readOnly = EditorState.readOnly.of(true);

// Custom styling
const customTheme = EditorView.theme({
  '&': {
    fontSize: '14px',
    border: '1px solid #333',
    borderRadius: '8px',
  },
  '.cm-content': {
    fontFamily: '"Fira Code", monospace',
    padding: '12px',
  },
  '.cm-gutters': {
    backgroundColor: '#1a1a2e',
    borderRight: '1px solid #333',
  },
  '.cm-activeLineGutter': {
    backgroundColor: '#2a2a4e',
  },
  '&.cm-focused .cm-cursor': {
    borderLeftColor: '#60a5fa',
  },
  '.cm-selectionBackground': {
    backgroundColor: '#3a3a6e !important',
  },
});

// Combine extensions
const extensions = [
  basicSetup,
  javascript({ typescript: true }),
  oneDark,
  tabExtension,
  customKeymap,
  customTheme,
  EditorView.lineWrapping,
];

Fix 5: Autocomplete and Linting

import { autocompletion, CompletionContext } from '@codemirror/autocomplete';
import { linter, type Diagnostic } from '@codemirror/lint';

// Custom autocomplete
function myCompletions(context: CompletionContext) {
  const word = context.matchBefore(/\w*/);
  if (!word || (word.from === word.to && !context.explicit)) return null;

  return {
    from: word.from,
    options: [
      { label: 'console.log', type: 'function', detail: 'Log to console' },
      { label: 'useState', type: 'function', detail: 'React hook' },
      { label: 'useEffect', type: 'function', detail: 'React hook' },
      { label: 'async', type: 'keyword' },
      { label: 'await', type: 'keyword' },
    ],
  };
}

const completionExtension = autocompletion({ override: [myCompletions] });

// Custom linter
const myLinter = linter((view) => {
  const diagnostics: Diagnostic[] = [];
  const text = view.state.doc.toString();

  // Check for console.log
  const regex = /console\.log/g;
  let match;
  while ((match = regex.exec(text)) !== null) {
    diagnostics.push({
      from: match.index,
      to: match.index + match[0].length,
      severity: 'warning',
      message: 'Remove console.log before committing',
    });
  }

  return diagnostics;
});

// Add to extensions
const extensions = [basicSetup, completionExtension, myLinter];

Fix 6: Dynamic Language Switching

'use client';

import { useState, useRef, useEffect } from 'react';
import { EditorView, basicSetup } from 'codemirror';
import { EditorState, Compartment } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
import { html } from '@codemirror/lang-html';

function DynamicLanguageEditor() {
  const containerRef = useRef<HTMLDivElement>(null);
  const viewRef = useRef<EditorView | null>(null);
  const langCompartment = useRef(new Compartment());
  const [language, setLanguage] = useState('typescript');

  useEffect(() => {
    if (!containerRef.current) return;

    const view = new EditorView({
      state: EditorState.create({
        doc: '// Start typing...',
        extensions: [
          basicSetup,
          langCompartment.current.of(javascript({ typescript: true })),
        ],
      }),
      parent: containerRef.current,
    });

    viewRef.current = view;
    return () => view.destroy();
  }, []);

  // Switch language without recreating the editor
  useEffect(() => {
    const view = viewRef.current;
    if (!view) return;

    const langMap: Record<string, any> = {
      typescript: javascript({ typescript: true, jsx: true }),
      javascript: javascript({ jsx: true }),
      python: python(),
      html: html(),
    };

    view.dispatch({
      effects: langCompartment.current.reconfigure(langMap[language] || javascript()),
    });
  }, [language]);

  return (
    <div>
      <select value={language} onChange={(e) => setLanguage(e.target.value)}>
        <option value="typescript">TypeScript</option>
        <option value="javascript">JavaScript</option>
        <option value="python">Python</option>
        <option value="html">HTML</option>
      </select>
      <div ref={containerRef} style={{ height: '400px' }} />
    </div>
  );
}

Still Not Working?

Editor renders but no syntax highlighting — you need a language extension. basicSetup provides editor features but not language support. Install and add @codemirror/lang-javascript (or the appropriate language) to extensions.

React state does not update when typing — CodeMirror manages its own state. Use EditorView.updateListener.of() to listen for changes and update React state. Do not try to make CodeMirror a controlled component — treat it as uncontrolled with sync.

Editor flickers or recreates on every render — the EditorView should be created once in useEffect, not on every render. Store it in a ref. For dynamic changes (language, theme), use Compartment to reconfigure without recreating.

Tab key inserts focus change instead of indentbasicSetup does not include tab-to-indent to preserve accessibility. Import and add keymap.of([indentWithTab]) from @codemirror/commands to enable tab indentation.

Editor mounts but has zero heightEditorView measures the parent at mount. If the container is display: none or height: 0 at the moment of construction, content lays out at zero height. Render the editor only after the container is visible, or call view.requestMeasure() once the layout settles.

Wrong CodeMirror version installed (CM5 vs CM6) — if import CodeMirror from 'codemirror' works and you can configure via an options object, you are on CM5. If you have to import EditorView and EditorState separately, you are on CM6. Pin the version in package.json and align your tutorial.

IME composition characters disappear (Japanese, Chinese, Korean input) — pre-6.5 versions had bugs with composition events. Upgrade to @codemirror/[email protected]+ and ensure no extension is calling view.dispatch synchronously during a compositionstart/compositionend window.

For related editor issues, see Fix: Monaco Editor Not Working, Fix: Tiptap Not Working, Fix: Prism React Renderer Not Working, and Fix: Shiki Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles