@uiw/react-codemirror 和 react-codemirror2 都是用于在 React 应用中集成 CodeMirror 代码编辑器的封装库,但它们基于完全不同的核心版本。@uiw/react-codemirror 基于现代化的 CodeMirror 6 构建,采用了全新的扩展系统和状态管理架构,性能更优且更易维护。react-codemirror2 则主要基于 CodeMirror 5,保留了经典的配置方式,适合旧项目维护,但核心引擎已处于维护模式,不再接受新功能更新。
在 React 项目中集成代码编辑器时,@uiw/react-codemirror 和 react-codemirror2 是最常见的两个选项。虽然它们的目标相同,但底层依赖的差异决定了它们在实际工程中的表现完全不同。让我们从核心架构、配置方式、扩展能力等维度进行深入对比。
这是两者最根本的区别,直接影响性能、打包体积和功能特性。
@uiw/react-codemirror 基于 CodeMirror 6。
// @uiw/react-codemirror: 基于 CM6
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
function Editor() {
return (
<CodeMirror
value="console.log('hello')"
height="200px"
extensions={[javascript()]}
/>
);
}
react-codemirror2 基于 CodeMirror 5。
// react-codemirror2: 基于 CM5
import { Controlled as CodeMirror } from 'react-codemirror2';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/javascript/javascript';
function Editor() {
return (
<CodeMirror
value="console.log('hello')"
options={{
mode: 'javascript',
lineNumbers: true
}}
/>
);
}
CodeMirror 6 引入了扩展(Extensions)系统,而 CM5 使用配置对象(Options)。这导致两者在启用功能时的写法完全不同。
@uiw/react-codemirror 使用 extensions 数组。
// @uiw/react-codemirror: 使用 extensions
import { readOnly } from '@codemirror/state';
import { oneDark } from '@codemirror/theme-one-dark';
<CodeMirror
value={code}
extensions={[
javascript(),
oneDark,
readOnly ? readOnly() : []
]}
/>
react-codemirror2 使用 options 对象。
// react-codemirror2: 使用 options 对象
<CodeMirror
value={code}
options={{
mode: 'javascript',
theme: 'darcula',
readOnly: true,
lineNumbers: true
}}
/>
两者都支持受控组件模式,但在处理变更事件时的细节有所不同。
@uiw/react-codemirror 提供标准的 onChange 回调。
// @uiw/react-codemirror: 丰富的 onChange 参数
<CodeMirror
value={code}
onChange={(value, view) => {
console.log('Content:', value);
// 可以访问 view.state 获取光标等详细信息
}}
/>
react-codemirror2 提供标准的 onChange 回调。
// react-codemirror2: 传统 onChange 参数
<CodeMirror
value={code}
onChange={(editor, data, value) => {
console.log('Content:', value);
// 需要 editor 实例来获取光标等信息
}}
/>
在添加语言支持、主题或插件时,两者的加载机制差异巨大。
@uiw/react-codemirror 支持 ES Module 按需导入。
// @uiw/react-codemirror: 按需导入语言包
import { cpp } from '@codemirror/lang-cpp';
import { githubLight } from '@uiw/codemirror-theme-github';
<CodeMirror
extensions={[cpp(), githubLight]}
/>
react-codemirror2 依赖 全局脚本加载或 Side Effects。
// react-codemirror2: 导入即注册到全局
import 'codemirror/mode/clike/clike';
import 'codemirror/theme/darcula.css';
// 然后在 options 中引用字符串名称
<CodeMirror
options={{ mode: 'text/x-c++src', theme: 'darcula' }}
/>
选择库不仅看现在,还要看未来。核心引擎的生命周期至关重要。
@uiw/react-codemirror 处于 活跃维护期。
// @uiw/react-codemirror: 持续获得新特性支持
// 例如新的自动补全接口、虚拟滚动优化等
react-codemirror2 处于 维护模式。
// react-codemirror2: 功能已冻结
// 无法使用 CM6 的新 API,如新的自动补全系统
| 特性 | @uiw/react-codemirror | react-codemirror2 |
|---|---|---|
| 核心版本 | CodeMirror 6 (现代) | CodeMirror 5 (经典) |
| 配置方式 | Extensions 数组 | Options 对象 |
| 打包优化 | 支持 Tree Shaking | 较难优化,依赖全局 |
| 类型支持 | 完善的 TypeScript 类型 | 类型支持较弱 |
| 移动端支持 | 优秀 | 一般 |
| 维护状态 | 活跃更新 | 维护模式 |
| 适用场景 | 新项目、高性能需求 | 旧项目维护、迁移过渡 |
@uiw/react-codemirror 是当前的标准选择 🏆。它基于 CodeMirror 6,提供了更好的性能、更小的体积和更现代化的 API。对于所有新启动的项目,都应该优先考虑它。虽然学习曲线稍陡(需要理解 Extensions 概念),但长期收益巨大。
react-codemirror2 仅作为遗留系统的过渡方案 🕰️。如果你的团队正在维护一个基于 CM5 的大型系统,且重构成本过高,可以继续使用它。但应制定计划,逐步迁移到 CM6 架构,以避免未来的技术债务。
核心结论:除非被旧代码锁死,否则不要在新项目中使用 react-codemirror2。CodeMirror 6 的架构优势 — 尤其是状态管理和扩展系统 — 使得 @uiw/react-codemirror 在工程化实践中更具优势。
如果你正在启动新项目,或者需要 CodeMirror 6 带来的高性能、模块化扩展系统和更好的移动端支持,请选择 @uiw/react-codemirror。它是面向未来的选择,能够充分利用现代 JavaScript 特性,且社区活跃,修复问题更及时。
仅在维护基于 CodeMirror 5 的遗留系统时选择 react-codemirror2。如果你的项目严重依赖 CM5 特有的插件或配置方式,且迁移成本过高,它可以作为过渡方案,但不建议用于任何新开发的功能。
CodeMirror component for React. Demo Preview: @uiwjs.github.io/react-codemirror
Features:
🚀 Quickly and easily configure the API.
🌱 Versions after @uiw/react-codemirror@v4 use codemirror 6. #88.
⚛️ Support the features of React Hook(requires React 16.8+).
📚 Use Typescript to write, better code hints.
🌐 The bundled version supports use directly in the browser #267.
🌎 There are better sample previews.
🎨 Support theme customization, provide theme editor.
🧑💻 SwiftUI wrapper for CodeMirror 6.
Not dependent on uiw.
npm install @uiw/react-codemirror --save
All Packages
import React from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
function App() {
const [value, setValue] = React.useState("console.log('hello world!');");
const onChange = React.useCallback((val, viewUpdate) => {
console.log('val:', val);
setValue(val);
}, []);
return <CodeMirror value={value} height="200px" extensions={[javascript({ jsx: true })]} onChange={onChange} />;
}
export default App;
import CodeMirror from '@uiw/react-codemirror';
import { StreamLanguage } from '@codemirror/language';
import { go } from '@codemirror/legacy-modes/mode/go';
const goLang = `package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}`;
export default function App() {
return <CodeMirror value={goLang} height="200px" extensions={[StreamLanguage.define(go)]} />;
}
@codemirror/legacy-modes/mode/cpp@codemirror/lang-cpp@codemirror/legacy-modes/mode/html@codemirror/lang-html@codemirror/legacy-modes/mode/java@codemirror/lang-java@codemirror/legacy-modes/mode/go@codemirror/lang-go@codemirror/legacy-modes/mode/javascript@codemirror/lang-javascript@codemirror/legacy-modes/mode/json@codemirror/lang-json@codemirror/legacy-modes/mode/lezer@codemirror/lang-lezer@codemirror/legacy-modes/mode/markdown@codemirror/lang-markdown@codemirror/legacy-modes/mode/php@codemirror/lang-php@codemirror/legacy-modes/mode/python@codemirror/lang-python@codemirror/legacy-modes/mode/rust@codemirror/lang-rust@codemirror/legacy-modes/mode/sql@codemirror/lang-sql@codemirror/legacy-modes/mode/xml@codemirror/lang-xml@codemirror/legacy-modes/mode/css@codemirror/lang-less@codemirror/legacy-modes/mode/sass@codemirror/lang-sass@codemirror/legacy-modes/mode/clojure@nextjournal/lang-clojure@codemirror/legacy-modes/mode/clike@replit/codemirror-lang-csharpMarkdown language code is automatically highlighted.
import CodeMirror from '@uiw/react-codemirror';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { languages } from '@codemirror/language-data';
const code = `## Title
\`\`\`jsx
function Demo() {
return <div>demo</div>
}
\`\`\`
\`\`\`bash
# Not dependent on uiw.
npm install @codemirror/lang-markdown --save
npm install @codemirror/language-data --save
\`\`\`
[weisit ulr](https://uiwjs.github.io/react-codemirror/)
\`\`\`go
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
\`\`\`
`;
export default function App() {
return <CodeMirror value={code} extensions={[markdown({ base: markdownLanguage, codeLanguages: languages })]} />;
}
A component that highlights the changes between two versions of a file in a side-by-side view, highlighting added, modified, or deleted lines of code.
npm install react-codemirror-merge --save
import CodeMirrorMerge from 'react-codemirror-merge';
import { EditorView } from 'codemirror';
import { EditorState } from '@codemirror/state';
const Original = CodeMirrorMerge.Original;
const Modified = CodeMirrorMerge.Modified;
let doc = `one
two
three
four
five`;
export const Example = () => {
return (
<CodeMirrorMerge>
<Original value={doc} />
<Modified
value={doc.replace(/t/g, 'T') + 'Six'}
extensions={[EditorView.editable.of(false), EditorState.readOnly.of(true)]}
/>
</CodeMirrorMerge>
);
};
import { useEffect, useMemo, useRef } from 'react';
import { useCodeMirror } from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
const code = "console.log('hello world!');\n\n\n";
// Define the extensions outside the component for the best performance.
// If you need dynamic extensions, use React.useMemo to minimize reference changes
// which cause costly re-renders.
const extensions = [javascript()];
export default function App() {
const editor = useRef();
const { setContainer } = useCodeMirror({
container: editor.current,
extensions,
value: code,
});
useEffect(() => {
if (editor.current) {
setContainer(editor.current);
}
}, [editor.current]);
return <div ref={editor} />;
}
We have created a theme editor where you can define your own theme. We have also defined some themes ourselves, which can be installed and used directly. Below is a usage example:
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { okaidia } from '@uiw/codemirror-theme-okaidia';
const extensions = [javascript({ jsx: true })];
export default function App() {
return <CodeMirror value="console.log('hello world!');" height="200px" theme={okaidia} extensions={extensions} />;
}
import CodeMirror from '@uiw/react-codemirror';
import { createTheme } from '@uiw/codemirror-themes';
import { javascript } from '@codemirror/lang-javascript';
import { tags as t } from '@lezer/highlight';
const myTheme = createTheme({
theme: 'light',
settings: {
background: '#ffffff',
backgroundImage: '',
foreground: '#75baff',
caret: '#5d00ff',
selection: '#036dd626',
selectionMatch: '#036dd626',
lineHighlight: '#8a91991a',
gutterBackground: '#fff',
gutterForeground: '#8a919966',
},
styles: [
{ tag: t.comment, color: '#787b8099' },
{ tag: t.variableName, color: '#0080ff' },
{ tag: [t.string, t.special(t.brace)], color: '#5c6166' },
{ tag: t.number, color: '#5c6166' },
{ tag: t.bool, color: '#5c6166' },
{ tag: t.null, color: '#5c6166' },
{ tag: t.keyword, color: '#5c6166' },
{ tag: t.operator, color: '#5c6166' },
{ tag: t.className, color: '#5c6166' },
{ tag: t.definition(t.typeName), color: '#5c6166' },
{ tag: t.typeName, color: '#5c6166' },
{ tag: t.angleBracket, color: '#5c6166' },
{ tag: t.tagName, color: '#5c6166' },
{ tag: t.attributeName, color: '#5c6166' },
],
});
const extensions = [javascript({ jsx: true })];
export default function App() {
const onChange = React.useCallback((value, viewUpdate) => {
console.log('value:', value);
}, []);
return (
<CodeMirror
value="console.log('hello world!');"
height="200px"
theme={myTheme}
extensions={extensions}
onChange={onChange}
/>
);
}
initialState to restore state from JSON-serialized representationCodeMirror allows to serialize editor state to JSON representation with toJSON function for persistency or other needs. This JSON representation can be later used to recreate ReactCodeMirror component with the same internal state.
For example, this is how undo history can be saved in the local storage, so that it remains after the page reloads
import CodeMirror from '@uiw/react-codemirror';
import { historyField } from '@codemirror/commands';
// When custom fields should be serialized, you can pass them in as an object mapping property names to fields.
// See [toJSON](https://codemirror.net/docs/ref/#state.EditorState.toJSON) documentation for more details
const stateFields = { history: historyField };
export function EditorWithInitialState() {
const serializedState = localStorage.getItem('myEditorState');
const value = localStorage.getItem('myValue') || '';
return (
<CodeMirror
value={value}
initialState={
serializedState
? {
json: JSON.parse(serializedState || ''),
fields: stateFields,
}
: undefined
}
onChange={(value, viewUpdate) => {
localStorage.setItem('myValue', value);
const state = viewUpdate.state.toJSON(stateFields);
localStorage.setItem('myEditorState', JSON.stringify(state));
}}
/>
);
}
value?: string value of the auto created model in the editor.width?: string width of editor. Defaults to auto.height?: string height of editor. Defaults to auto.theme?: 'light' / 'dark' / Extension Defaults to 'light'.import React from 'react';
import { EditorState, EditorStateConfig, Extension } from '@codemirror/state';
import { EditorView, ViewUpdate } from '@codemirror/view';
export * from '@codemirror/view';
export * from '@codemirror/basic-setup';
export * from '@codemirror/state';
export declare const ExternalChange: import('@codemirror/state').AnnotationType<boolean>;
export interface UseCodeMirror extends ReactCodeMirrorProps {
container?: HTMLDivElement | null;
}
export declare function useCodeMirror(props: UseCodeMirror): {
state: EditorState | undefined;
setState: import('react').Dispatch<import('react').SetStateAction<EditorState | undefined>>;
view: EditorView | undefined;
setView: import('react').Dispatch<import('react').SetStateAction<EditorView | undefined>>;
container: HTMLDivElement | null | undefined;
setContainer: import('react').Dispatch<import('react').SetStateAction<HTMLDivElement | null | undefined>>;
};
export interface ReactCodeMirrorProps
extends
Omit<EditorStateConfig, 'doc' | 'extensions'>,
Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'placeholder'> {
/** value of the auto created model in the editor. */
value?: string;
height?: string;
minHeight?: string;
maxHeight?: string;
width?: string;
minWidth?: string;
maxWidth?: string;
/** focus on the editor. */
autoFocus?: boolean;
/** Enables a placeholder—a piece of example content to show when the editor is empty. */
placeholder?: string | HTMLElement;
/**
* `light` / `dark` / `Extension` Defaults to `light`.
* @default light
*/
theme?: 'light' | 'dark' | Extension;
/**
* Whether to optional basicSetup by default
* @default true
*/
basicSetup?: boolean | BasicSetupOptions;
/**
* This disables editing of the editor content by the user.
* @default true
*/
editable?: boolean;
/**
* This disables editing of the editor content by the user.
* @default false
*/
readOnly?: boolean;
/**
* Controls whether pressing the `Tab` key inserts a tab character and indents the text (`true`)
* or behaves according to the browser's default behavior (`false`).
* @default true
*/
indentWithTab?: boolean;
/** Fired whenever a change occurs to the document. */
onChange?(value: string, viewUpdate: ViewUpdate): void;
/** Some data on the statistics editor. */
onStatistics?(data: Statistics): void;
/** The first time the editor executes the event. */
onCreateEditor?(view: EditorView, state: EditorState): void;
/** Fired whenever any state change occurs within the editor, including non-document changes like lint results. */
onUpdate?(viewUpdate: ViewUpdate): void;
/**
* Extension values can be [provided](https://codemirror.net/6/docs/ref/#state.EditorStateConfig.extensions) when creating a state to attach various kinds of configuration and behavior information.
* They can either be built-in extension-providing objects,
* such as [state fields](https://codemirror.net/6/docs/ref/#state.StateField) or [facet providers](https://codemirror.net/6/docs/ref/#state.Facet.of),
* or objects with an extension in its `extension` property. Extensions can be nested in arrays arbitrarily deep—they will be flattened when processed.
*/
extensions?: Extension[];
/**
* If the view is going to be mounted in a shadow root or document other than the one held by the global variable document (the default), you should pass it here.
* Originally from the [config of EditorView](https://codemirror.net/6/docs/ref/#view.EditorView.constructor%5Econfig.root)
*/
root?: ShadowRoot | Document;
/**
* Create a state from its JSON representation serialized with [toJSON](https://codemirror.net/docs/ref/#state.EditorState.toJSON) function
*/
initialState?: {
json: any;
fields?: Record<'string', StateField<any>>;
};
}
export interface ReactCodeMirrorRef {
editor?: HTMLDivElement | null;
state?: EditorState;
view?: EditorView;
}
declare const ReactCodeMirror: React.ForwardRefExoticComponent<
ReactCodeMirrorProps & React.RefAttributes<ReactCodeMirrorRef>
>;
export default ReactCodeMirror;
export interface BasicSetupOptions {
lineNumbers?: boolean;
highlightActiveLineGutter?: boolean;
highlightSpecialChars?: boolean;
history?: boolean;
foldGutter?: boolean;
drawSelection?: boolean;
dropCursor?: boolean;
allowMultipleSelections?: boolean;
indentOnInput?: boolean;
syntaxHighlighting?: boolean;
bracketMatching?: boolean;
closeBrackets?: boolean;
autocompletion?: boolean;
rectangularSelection?: boolean;
crosshairCursor?: boolean;
highlightActiveLine?: boolean;
highlightSelectionMatches?: boolean;
closeBracketsKeymap?: boolean;
defaultKeymap?: boolean;
searchKeymap?: boolean;
historyKeymap?: boolean;
foldKeymap?: boolean;
completionKeymap?: boolean;
lintKeymap?: boolean;
}
import { EditorSelection, SelectionRange } from '@codemirror/state';
import { ViewUpdate } from '@codemirror/view';
export interface Statistics {
/** Get the number of lines in the editor. */
lineCount: number;
/** total length of the document */
length: number;
/** Get the proper [line-break](https://codemirror.net/docs/ref/#state.EditorState^lineSeparator) string for this state. */
lineBreak: string;
/** Returns true when the editor is [configured](https://codemirror.net/6/docs/ref/#state.EditorState^readOnly) to be read-only. */
readOnly: boolean;
/** The size (in columns) of a tab in the document, determined by the [`tabSize`](https://codemirror.net/6/docs/ref/#state.EditorState^tabSize) facet. */
tabSize: number;
/** Cursor Position */
selection: EditorSelection;
/** Make sure the selection only has one range. */
selectionAsSingle: SelectionRange;
/** Retrieves a list of all current selections. */
ranges: readonly SelectionRange[];
/** Get the currently selected code. */
selectionCode: string;
/**
* The length of the given array should be the same as the number of active selections.
* Replaces the content of the selections with the strings in the array.
*/
selections: string[];
/** Return true if any text is selected. */
selectedText: boolean;
}
export declare const getStatistics: (view: ViewUpdate) => Statistics;
$ npm install # Installation dependencies
$ npm run build # Compile all package
@uiw/react-codemirror package:$ cd core
# listen to the component compile and output the .js file
# listen for compilation output type .d.ts file
$ npm run watch # Monitor the compiled package `@uiw/react-codemirror`
npm run start
As always, thanks to our amazing contributors!
Made with contributors.
Licensed under the MIT License.