Bayan Bennett

Failing to add CodeMirror 6 (and then Succeeding) | DevLog 004

TypeScript
React JS
CodeMirror
Video version of this post

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 setViewcan 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.

© 2022 Bayan Bennett