Failing to add CodeMirror 6 (and then Succeeding)
Bringing a non-React project into a React project
In the last post I created a React component that intercepted and formatted the arguments for console.log
and console.error
.
In this post Iβll be upgrading from a simple <textarea>
to something that is designed to show code.
What Should I Use?
After mulling over a few different options, even considering making something from scratch, I decided that CodeMirror would be the best fit for short code snippets.
Fortunately, CodeMirror v6 is out and itβs a fantastic opportunity to try it out.
Creating the Editor Component
The editor component will live in src/components/editor/index.tsx
. For now, it will be an empty react component.
import React from "react";export const Editor = () => {
return <></>;
};
Weβll also need to install some libraries from npm
npm i @codemirror/view @codemirror/state
CodeMirror needs to be attached to a DOM node, so weβll add it via a ref
. Weβll also use a <section>
for some added semantics. In the world of TypeScript types, I learned that any HTML element that doesnβt need more than what the base interface provides, like <section>
, should use the HTMLElement
type.
import React from "react";export const Editor = () => {
const editorRef = React.useRef<HTMLElement>(null);
return <section ref="editorRef"/>;
};
editorRef
will be null
until the DOM node is selected, so a useEffect
is needed to attach the editor.
React.useEffect(() => {
if(editorRef.current === null) return;
const view = new EditorView({
state: EditorState.create({ doc: "hello" }),
parent: editorRef.current
});
}, [editorRef.current]);
Passing the State
Note: this section deviates from the video as the mistakes I made there are not particularly important to go over.
The component that will be using the <Editor>
will need to evaluate what is being typed in. In other words, the Editor will need to accept a callback function that it can call to pass the state to the parent.
I tried using EditorState["doc"]
and EditorState
, however, what worked was returning the EditorView
.
type EditorProps = {
setView => (view: EditorView) => void
}
I also realised that there was no need to have EditorState
declared inside the component, so I moved it out.
import React from "react";const state = EditorState.create({ doc: "hello" });export const Editor = () => { /* ... */ };
In the editor component setView
can be called as soon as the view is ready. Itβs also important to destroy the view when the editor is unmounted.
React.useEffect(() => {
if(editorRef.current === null) return; const view = new EditorView({
state: EditorState.create({ doc: "hello" }),
parent: editorRef.current
}); setView(view); return () => {
view.destroy();
setView(null);
};
}, [editorRef.current]);
The parent component can be updated to use the <Editor>
component as well as to update the code evaluator to use the EditorView.state.doc
.
import React from "react";
import { Editor } from "../../../components/editor";
import { EditorView } from "@codemirror/view";
const StringPage = () => {
const [view, setView] = React.useState<EditorView | null>(null);
/* ... */ const evaluateCode = () => {
if (view === null) return;
const code = view.state.doc.toString();
try {
new Function(code)();
} catch (e) {
console.error(e);
}
};
return (
<>
{/* irrelevant components omitted */}
<Editor setView={setView} />
</>
);
};
export default StringPage;
TL; DR
I put CodeMirror version 6 into a React component.