CodeMirror is a code editor component for the web. However it is not written in React. If you're building a react app and want to use CodeMirror, here's a guide on how to integrate the two, and remain highly responsive.
Let's start by installing CodeMirror and its dependencies:
npm install codemirror
We're going to create a basic editor that accepts props such as onChange
and onBlur
. Our goal it to be able to integrate with react state and props.
Create a file called editor.ts
and initialize the editor. Here you can expand on the editor configuration to include other plugins and themes.
import { EditorState } from '@codemirror/state'
import { EditorView } from '@codemirror/view'
const createEditor = (
parentElement: Element,
args: {
onChange?: (value: string) => void
onBlur?: () => void
onFocus?: () => void
}
) => {
const { onChange, onBlur, onFocus } = args
const state = EditorState.create({
extensions: [
// Handle changes
EditorView.updateListener.of((update) => {
if (update.docChanged && onChange) {
const value = update.state.doc.toString()
onChange(value)
}
}),
EditorView.domEventHandlers({
blur: onBlur,
focus: onFocus,
}),
],
})
const view = new EditorView({
state,
parent: parentElement,
})
return view
}
export default createEditor
Now let's create our React component which will render the editor.
import useCodeEditor from './useCodeEditor'
interface ICodeEditorViewProps {
value: string
onChange?: (value: string) => void
onBlur?: () => void
onFocus?: () => void
}
const CodeEditorView = (props: ICodeEditorViewProps) => {
const { value, onChange, onBlur, onFocus } = props
const { ref, editor } = useCodeEditor({
value,
onChange,
onBlur,
onFocus,
})
return <div ref={ref} />
}
export default CodeEditorView
Here we're using a custom hook to manage the editor state, this is where the magic happens. Let's build the hook useCodeEditor
. This will connect the editor to the react component. I've added plenty of comments to explain what's happening.
import { EditorView } from 'codemirror'
import { useEffect, useRef, useState } from 'react'
import createEditor from './editor'
// We use an event emitter to decouple the editor from the
// react lifecycle. This means we do not need to destroy and
// recreate the editor any time the props change.
//
// Instead we can subscribe and unsubscribe to events
// which is less expensive and more performant.
import createEventEmitter from './editorEvents'
const useCodeEditor = (args: {
value: string
onChange?: (value: string) => void
onBlur?: () => void
onFocus?: () => void
}) => {
const { value, onChange, onBlur, onFocus } = args
// Define a ref which will be passed to the editor.
// This is where the editor will be mounted.
const ref = useRef<HTMLDivElement>(null)
// Define a state to store the editor instance
const [editor, setEditor] = useState<EditorView>()
// Define a unique event emitter for each editor instance
const emitterRef = useRef(createEventEmitter())
// When the value changes we can call the editor's dispatch
// method to reflect the new value in the editor.
//
// We can keep this simple and just replace the entire
// editor value with the new value.
useEffect(() => {
if (editor && (value || value === '')) {
const editorValue = editor.state.doc.toString()
// Prevent infinite loop
if (editorValue === value) {
return
}
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: value,
},
})
}
}, [value, editor])
// Initialize the editor once. Without event emitters
// you would have to destroy and recreate the editor
// any time the props change.
useEffect(() => {
const editorEvents = emitterRef.current
const editorView = createEditor(ref.current!, {
onChange: (value) => editorEvents.emit('change', value),
onBlur: () => editorEvents.emit('blur'),
onFocus: () => editorEvents.emit('focus'),
})
setEditor(editorView)
return () => {
editorView.destroy()
setEditor(undefined)
}
}, [setEditor])
// Subscribe to editor events when the props change
// Ensuring we clean up and remove the listeners when
// the component is unmounted.
useEffect(() => {
const editorEvents = emitterRef.current
const changeListener = (value: string) => onChange?.(value)
editorEvents.on('change', changeListener)
const blurListener = () => onBlur?.()
editorEvents.on('blur', blurListener)
const focusListener = () => onFocus?.()
editorEvents.on('focus', focusListener)
return () => {
editorEvents.off('change', changeListener)
editorEvents.off('blur', blurListener)
editorEvents.off('focus', focusListener)
}
}, [onChange, onBlur, onFocus])
return { ref, editor }
}
export default useCodeEditor
Finally we need to create the event emitter. This is a simple file which outlines the available event types. We're using eventemitter3
for this.
import { EventEmitter } from 'eventemitter3'
// Define the types of events and their corresponding payloads
interface EditorEvents {
change: (value: string) => void
blur: () => void
focus: () => void
}
const createEmitter = () => new EventEmitter<EditorEvents>()
export default createEmitter
By using an event emitter we can decouple the editor from the react lifecycle. This means we do not need to destroy the editor any time a callback prop changes. Instead we can subscribe and unsubscribe to events when props change.
This apparoch is far less expensive and more performant. The browser is simply doing less work.
Happy coding!