Building an Interactive Code Editor with React and CodeMirror
Creating a fully interactive code editor in a web app can feel like a complex task. However, with the right tools, you can build a responsive, user-friendly editor that manages dynamic content seamlessly. In this tutorial, I’ll walk you through building a code editor using @uiw/react-codemirror
— a powerful, customizable tool that makes setting up syntax highlighting, real-time content changes, and editor configurations straightforward.
Why Share This?
I encountered various challenges while developing my own project, CodeLib, particularly due to verbose documentation on the CodeMirror site, which can be hard to follow for straightforward use cases. With this tutorial, I hope to simplify the process for anyone implementing code editors in their projects.
What You'll Learn
- Setting up CodeMirror in a React/Next.js application.
- Customizing the editor with syntax highlighting and theme support.
- Managing dynamic content updates in the editor.
Requirements
This tutorial uses Next.js, but the setup is suitable for any React-based framework. You’ll need some familiarity with React and TypeScript.
Step 1 - Setting Up CodeMirror
To start, we’ll create a basic code editor component using @uiw/react-codemirror
, which simplifies setup compared to react-codemirror
. First, install the necessary packages:
npm install @uiw/react-codemirror @codemirror/lang-javascript
Then, create a new component and import CodeMirror
:
import CodeMirror from "@uiw/react-codemirror";
const CodeEditor = () => {
return (
<CodeMirror height="600px" theme="dark" />
);
};
export default CodeEditor;
This provides a basic editor which we’ll customize.
Step 2 - Adding Syntax Highlighting
To add syntax highlighting for different languages, import language modules and define extensions:
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 { markdown } from '@codemirror/lang-markdown';
const languageExtensions = {
js: javascript(),
python: python(),
html: html(),
css: css(),
markdown: markdown()
};
Define a helper function to match file extensions to languages:
const getLanguageExtension = (filename: string) => {
const extension = filename.split('.').pop();
return languageExtensions[extension || ''] || javascript();
};
Then pass the correct language extension to CodeMirror
:
<CodeMirror extensions={[getLanguageExtension(filename)]} theme="dark" />
Step 3 - Handling Dynamic Content Updates
Dynamic content can be handled with useState
and useEffect
. Here’s an example:
import { useState, useEffect, useCallback, useRef } from 'react';
const CodeEditor = ({ filename = "example.js", content, onSave }) => {
const [value, setValue] = useState(content);
const editorRef = useRef(null);
useEffect(() => {
if (editorRef.current?.view) {
const currentValue = editorRef.current.view.state.doc.toString();
if (currentValue !== content) {
editorRef.current.view.dispatch({
changes: { from: 0, to: currentValue.length, insert: content },
});
setValue(content);
}
}
}, [content]);
const handleChange = (val) => setValue(val);
return (
<CodeMirror
ref={editorRef}
value={value}
extensions={[getLanguageExtension(filename)]}
theme="dark"
onChange={handleChange}
height="600px"
/>
);
};
This syncs content
with the editor and allows for dynamic updates.
Step 4 - Saving Content with Keyboard Shortcuts
You can add a keyboard shortcut to save content using useEffect
:
useEffect(() => {
const handleKeyDown = (event) => {
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
onSave(filename, value);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [filename, value, onSave]);
This listens for Ctrl + S
to save the editor content.
Full Component Code
Here’s the complete code for the dynamic CodeEditor component, based on my personal use case. Feel free to more of my code on GitHub.
'use client';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import CopyButton from '@/components/snippets/CopyButton'
import CodeMirror from '@uiw/react-codemirror';
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 { markdown } from '@codemirror/lang-markdown';
import { java } from '@codemirror/lang-java';
import { cpp } from '@codemirror/lang-cpp';
import { json } from '@codemirror/lang-json';
import { php } from '@codemirror/lang-php';
import { rust } from '@codemirror/lang-rust';
import { sql } from '@codemirror/lang-sql';
import { xml } from '@codemirror/lang-xml';
import { less } from '@codemirror/lang-less';
import { sass } from '@codemirror/lang-sass';
import { clojure } from '@nextjournal/lang-clojure';
import { csharp } from '@replit/codemirror-lang-csharp';
// Add more language imports as needed
// Import for ref
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
const languageExtensions: { [key: string]: any } = {
js: javascript({ jsx: true }),
jsx: javascript({ jsx: true }),
ts: javascript({ typescript: true }),
tsx: javascript({ typescript: true, jsx: true }),
py: python(),
html: html(),
css: css(),
md: markdown(),
java: java(),
cpp: cpp(),
json: json(),
php: php(),
rust: rust(),
sql: sql(),
xml: xml(),
less: less(),
sass: sass(),
clojure: clojure(),
csharp: csharp(),
// Add more mappings for other languages
};
const getLanguageExtension = (filename: string) => {
const extension = filename.split('.').pop();
return languageExtensions[extension || ''] || javascript(); // Default to JavaScript if the language is not supported
};
const CodeEditor = ({
filename = "example.js",
content,
onSave,
onContentChange,
editorRef
}: {
filename?: string,
content: string,
onSave: (filename: string, content: string) => void,
onContentChange?: (content: string) => void,
editorRef: React.RefObject<ReactCodeMirrorRef>
}) => {
const [value, setValue] = useState(content);
useEffect(() => {
// Check for editorRef.current.view before accessing it
if (editorRef.current?.view) {
const currentValue = editorRef.current.view.state.doc.toString();
if (currentValue !== content) {
console.log("Content Prop Change Detected, Updating Editor:", content);
console.log("Current CodeMirror Doc State:", currentValue);
setValue(content);
editorRef.current.view.dispatch({
changes: { from: 0, to: currentValue.length, insert: content },
});
}
}
}, [content, editorRef]);
const handleChange = useCallback((val: string) => {
setValue(val);
if (onContentChange) {
onContentChange(val);
}
}, [onContentChange]);
const handleKeyDown = useCallback((event: KeyboardEvent) => {
if (event.ctrlKey && event.key === 's') {
event.preventDefault(); // Prevent default save behavior
onSave(filename, value); // Call the save function with current content
}
}, [filename, value, onSave]);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
const languageExtension = useMemo(() => getLanguageExtension(filename), [filename]);
return (
<div>
<CodeMirror
ref={editorRef}
value={value}
height="600px"
extensions={[languageExtension]}
theme="dark"
onChange={handleChange}
style={{
lineHeight: '1.6',
}}
className='max-w-full text-sm md:text-lg'
/>
<div className='flex flex-row gap-4 items-center mt-4'>
<Button onClick={() => onSave(filename, value)} className="mt-4">
Save
</Button>
<CopyButton editorRef={editorRef} className="mt-4" />
</div>
</div>
);
};
export default CodeEditor;
Conclusion
In this tutorial, you’ve learned how to set up a dynamic code editor with @uiw/react-codemirror
, complete with language extensions, dynamic content handling, and keyboard shortcuts for saving. This setup provides a flexible and interactive editor ready to be integrated into your applications.
Let me know what topics you’d like me to cover next! Your feedback is valuable ♥
Happy Coding!
Top comments (3)
Woah! This one was a great article. Great job man :)
I'm surely visiting it often when I need to use Codemirror in React!
Keep it up with your content! You explain things clearly.
Thanks so much Francisco! Your kind words are greatly appreciated :)
The ability to mock APIs in EchoAPI is invaluable for my React projects, allowing for smooth testing without backend dependencies.
Some comments have been hidden by the post's author - find out more