Skip to content

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

FixDevs ·

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:

  • 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 doesn’t 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.

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 doesn’t update when typing — CodeMirror manages its own state. Use EditorView.updateListener.of() to listen for changes and update React state. Don’t 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 doesn’t include tab-to-indent to preserve accessibility. Import and add keymap.of([indentWithTab]) from @codemirror/commands to enable tab indentation.

For related editor issues, see Fix: Monaco Editor Not Working and Fix: Tiptap 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