Codiga has joined Datadog!

Read the Blog·

Interested in our Static Analysis?

Sign up
← All posts
Oscar Salazar Tuesday, October 11, 2022

Revisiting our CodeMirror 6 implementation in React after the official release

Share

AUTHOR

Oscar Salazar, Senior Software Engineer

Oscar is a Software Engineer passionate about frontend development and creative coding, he has worked in several projects involving from video games to rich interactive experiences in different web applications. He loves studying and playing with the newest CSS features to create fantastic art.

See all articles

A couple of months ago I wrote a blog post on how to implement CodeMirror 6 with React. Back then CodeMirror 6 was still in beta, and some of its API was prone to changes.

A couple of weeks ago CodeMirror 6 was officially released. Let's revisit what changed and how we can improve our previous implementation.

Basic CodeMirror 6 packages

CodeMirror 6 API didn't go through any major changes, and most of our implementation still works. The biggest change we need to deal with is in the packages. The basic setup package is now exported from codemirror instead of @codemirror/basicsetup. Go to your project and add it through your package manager.

Be aware that it will ask you to choose a version, make sure it's version 6, and verify it in your package.json. Sometimes if you have installed previous versions of CodeMirror it might not get updated.

Code highlighting in CodeMirror 6 and supported languages

To enable code highlighting, and suggestions we are going to need a language package. Official CodeMirror 6 languages come in separate packages like @codemirror/lang-javascript, @codemirror/lang-python, etc.

So far CodeMirror 6 offers support for CSS, C++, HTML, Java, JavaScript, JSON, Markdown, PHP, Python, Rust, and XML.

If you need a different language look for community packages or see if a CodeMirror 5 mode is available.

Adding CodeMirror 6 to React with a custom hook

If you don't need any custom behavior in your code editor the basic setup offers everything you need. Let's create a hook to use CoreMirror 6, and review what's included in the basic setup.

// use-code-mirror.ts
import { useState } from "react";
import { EditorView, basicSetup } from "codemirror";
import { javascript } from "@codemirror/lang-javascript";

export default function useCodeMirror(extensions) {
  const ref = useRef();
  const [view, setView] = useState();

  useEffect(() => {
    const view = new EditorView({
      extensions: [
        basicSetup,
        /**
         * Check each language package to see what they support,
         * for instance javascript can use typescript and jsx.
         */
        javascript({
          jsx: true,
          typescript: true,
        }),
        ...extensions,
      ],
      parent: ref,
    });

    setView(view);

    /**
     * Make sure to destroy the codemirror instance
     * when our components are unmounted.
     */
    return () => {
      view.destroy();
      setView(undefined);
    };
  }, []);

  return { ref, view };
}

Let's see what's included with basicSetup:

  • line numbers
  • highlight active gutter
  • highlight special characters
  • undo and redo history
  • fold gutters
  • draw selections
  • drop cursor at the dragged position
  • multiple selections
  • automatic indent
  • syntax highlighting
  • match brackets
  • close brackets
  • autocompletion
  • rectangular selection
  • crosshair cursor on key-down Alt
  • highlight active line
  • highlight selection matches
  • key maps to trigger brackets close, search, history, fold, completion, linting, and the default arrow keys, select all, etc.

As you see, the basicSetup package of CodeMirror 6 will give you all the core functionality of a code editor. All you need to do is set the ref returned by the hook and you are good to go.

Sometimes CodeMirror 6 basic setup can be a little too much, in case you need a minimal working code editor the codemirror package also exports a minimal configuration from { minimalSetup }.

The minimal setup only includes:

  • highlight special characters
  • undo and redo history
  • draw selections
  • syntax highlighting
  • keymaps to deal with history commands, and the default arrow keys, select all, etc.

At this point whether you implemented the basic or minimal setup our hook is not that useful in our React environment, we don't know what the value of the editor is and we can't change the state of the editor.

Thankfully we already have a mechanism in place, if you look at our useCodeMirror hook we accept an extension parameter that let us access the state of the editor. Let's dig into CodeMirror 6 extensions.

CodeMirror 6 Extensions pattern

The new way of extending CodeMirror 6 functionality is through extensions. The basicSetup is no longer enough for our needs, we now need to set the value of our editor and access its internal state, for that we will use the following packages.

@codemirror/state let us create new fields, storing any kind of information, for example, how many tooltips are open, and where are they positioned.

@codemirror/view let us change the editor theme, and user interface elements. The view will be in charge of dispatching state changes and how the editor looks in the browser.

@codemirror/commands let us create custom user input and actions. Here we have all the mappings of the keyboard, like selecting all text with the keys CMD + a.

@codemirror/[extention-name] common functionalities such as autocompletion, search, linting, and many more come in separate packages, they offer the founding blocks to extend even further the code editor.

This granular approach helps us optimize the bundle size by only importing what is strictly necessary for our code editor.

Creating CodeMirror 6 extensions to integrate React

Let's create an extension to set the value of our code editor via React props and access the code editor's internal state to catch the changes made by the user, with this hook we could use our code editor as an input field in a React form.

// on-update.ts
import { EditorView, ViewUpdate } from "@codemirror/view";

type OnChange = (value: string, viewUpdate: ViewUpdate) => void;

export function onUpdate(onChange: OnChange) {
  return EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
    if (vu.docChanged) {
      const doc = viewUpdate.state.doc;
      const value = doc.toString();
      onChange(value, viewUpdate);
    }
  });
}

It's that simple, we created a function that receives a callback, this callback will be our React onChange handler, we are listening to any CodeMirror change and checking if it was a change in the document content. In case there is a content change, we call our onChange handler with the new value of the document and conveniently provide the view update object.

Now let's create a React hook to use our extended CoreMirror 6 editor.

// use-code-editor.ts
import onUpdate from "./on-update";
import useCodeMirror from "./use-codemirror";

export function useCodeEditor({ value, onChange, extensions }) {
  const { ref, view } = useCodeMirror([onUpdate(onChange), ...extensions]);

  useEffect(() => {
    if (view) {
      const editorValue = view.state.doc.toString();

      if (value !== editorValue) {
        view.dispatch({
          changes: {
            from: 0,
            to: editorValue.length,
            insert: value || "",
          },
        });
      }
    }
  }, [value, view]);

  return ref;
}

We now have a hook that will update the value of the editor whenever we provide a different value from what's currently in the editor. We also implemented the onUpdate extension to listen to every document change.

Now all that's left is for you to create your component or attach the ref to whatever element you wish to mount CodeMirror 6.

As an example let's create a CodeEditor component.

// CodeEditor.tsx
import useCodeEditor from "./use-code-editor";

export default function CodeEditor({ value, onChange, extensions }) {
  const ref = useCodeEditor({ value, onChange, extensions });

  return <div ref={ref} />;
}

// and you can use it something like this:
// MyForm.tsx
import CodeEditor form "./CodeEditor.tsx"

export default function MyForm() {
  const [code, setCode] = useState("console.log");

  return (
    <form>
      <CodeEditor
        value={code}
        onChange={(newCode) => {
          setCode(newCode);
        }}
      />
    </form>
  );
}

It's always a good idea to put common behavior logic in a hook so that when you need a different layout you can simply implement your hook in another component. That's why we separated our implementation into a reusable useCodeMirror hook and a useCodeEditor hook.

useCodeMirror is intentionally small, by design all we want is to have a hook that creates a CodeMirror 6 instance with the basic code editor capabilities, and accepts any number of extensions. I would encourage you to move out the basic setup and language support extensions from this hook and implement them in a different one so that you can reuse this hook in any project without bringing undesired behavior.

useCodeEditor is a more complex hook, while still reusable, it might not be as easy to share across different applications depending on your needs. This hook comes with an opinionated way of handling React value and onChange props so that we can support a controlled state of the editor. It's up to you to decide what else to include in this hook, like theming capabilities, changing the language, disabling the editor, etc.

CodeEditor is a convenient component that you could use across your app. In case you need to wrap your editor in a custom layout for your application this would be the ideal place.

Wrapping it up

We are done! we have a fully extensible code editor with support for mobile devices in our React application. In this post, we saw the new distribution packages of CodeMirror 6, adjusted our previous hook, and made a code editor hook with support for a controlled state through React.

In upcoming posts, we will revisit our autocomplete implementation with asynchronous data fetching, how to use custom tooltips like code annotations in CodeMirror 6 and React, and code animations, stay tuned!

Are you interested in Datadog Static Analysis?

Sign up