loading...
Cover image for Creating a React code editor and syntax highlighter

Creating a React code editor and syntax highlighter

kendalmintcode profile image Rob Kendal {{☕}} Originally published at robkendal.co.uk on ・5 min read

Building a React-based syntax highlighter blog article header image

Fuelled by a workplace conundrum about finding a reliable, efficient means to do a simple job, without needing to bring in the heavy hammer of another dependency, I recently created a React-based code syntax highlighter.

Surely there's something else already out there?

Yes, yes there is. There are a few code syntax highlighting components out there, but there are a few things to consider when shopping around for a third-party component:

  1. Adding an extra dependency adds more code weight and potential security problems into your project. If you can avoid this, you should.
  2. If the task is fairly small or not too onerous from an effort point of view then it's worth building an in-house solution where possible.
  3. The existing third-party offerings can be either quite out of date or paid options (and the paid options are usually expensive).

Using the ever useful Prism JS made by the helpful Lea Verou, we built a simple, to the point syntax highlighter that tracks its own state and dynamically swaps the language highlighting as needed.

Without further ado, here's how to do it

Building the React-based code editor with syntax highlighter

First things first, get a React project up and running and then let's install Prism JS

npm i prismjs

// or

yarn add prismjs

Next we need to add our CodeEditor component to the main App.js file to kick everything else off.

import React, { useState } from "react";

// Styles
import "./styles.css";

// Components
import CodeEditor from "./CodeEditor";

export default function App() {
  const [editorLanguage, setEditorLanguage] = useState("javascript");

  return (
    <div className="App">
      <h1>React code syntax hightlighter</h1>

      <fieldset>
        <legend>Choose language:</legend>
        <input
          type="radio"
          id="javascript"
          name="language"
          value="javascript"
          checked={editorLanguage === "javascript"}
          onChange={() => setEditorLanguage("javascript")}
        />
        <label htmlFor="javascript">JavaScript</label>
        <input
          type="radio"
          id="xml"
          name="language"
          value="markup"
          checked={editorLanguage === "markup"}
          onChange={() => setEditorLanguage("markup")}
        />
        <label htmlFor="xml">XML</label>
        <input
          type="radio"
          id="css"
          name="language"
          value="css"
          checked={editorLanguage === "css"}
          onChange={() => setEditorLanguage("css")}
        />
        <label htmlFor="css">CSS</label>
      </fieldset>

      <CodeEditor language={editorLanguage} />
    </div>
  );
}

Nothing too tricky going on here. We're adding useState from React to keep track of our language selection. Speaking of which, we've also got some simple radio button elements that update our language selection into state.

When a user selects a different language, we update their choice in state and then pass this along to our CodeEditor component which will, eventually, call Prism to update the syntax highlighting.

One caveat to watch out for here is to make sure you add the checked property to the radio buttons and compare that radio button's language with the current state value. This relationship between state values and form fields turns ordinary form fields into controlled components.

Now, although we haven't created the CodeEditor component yet (we'll do that next), we've finished off the main App component with all the necessary bits we need.

Creating the CodeEditor component

Now we come to the main event, the syntax highlighter itself, the CodeEditor component.

Here it is in full:

import React, { useState, useEffect } from "react";
import Prism from "prismjs";

const CodeEditor = props => {
  const [content, setContent] = useState(props.content);

  const handleKeyDown = evt => {
    let value = content,
      selStartPos = evt.currentTarget.selectionStart;

    console.log(evt.currentTarget);

    // handle 4-space indent on
    if (evt.key === "Tab") {
      value =
        value.substring(0, selStartPos) +
        " " +
        value.substring(selStartPos, value.length);
      evt.currentTarget.selectionStart = selStartPos + 3;
      evt.currentTarget.selectionEnd = selStartPos + 4;
      evt.preventDefault();

      setContent(value);
    }
  };

  useEffect(() => {
    Prism.highlightAll();
  }, []);

  useEffect(() => {
    Prism.highlightAll();
  }, [props.language, content]);

  return (
    <div className="code-edit-container">
      <textarea
        className="code-input"
        value={content}
        onChange={evt => setContent(evt.target.value)}
        onKeyDown={handleKeyDown}
      />
      <pre className="code-output">
        <code className={`language-${props.language}`}>{content}</code>
      </pre>
    </div>
  );
};

export default CodeEditor;

It's not too big or complex of a component, but let's break it down.

First, we import the useEffect and useState hooks from React as well as importing the PrismJS module.

We're using useState to track updates to our input, for which we're using a text area element. We also output the Prism-styled input into a pre block as per Prism JS's documentation.

<pre className="code-output">
  <code className={`language-${props.language}`}>{content}</code>
</pre>

useEffect replaces many React lifecycle functions, such as componentDidMount(). For our purposes, we're essentially watching changes to both the language passed in via props, and our input changes. If either happens, we fire Prism's highlightAll function to update the styling.

useEffect(() => {
  Prism.highlightAll();
}, [props.language, content]);

Which is very neat and effective. One of the benefits of React Hooks!

The most interesting part is what happens on the onKeyDown event:

const handleKeyDown = evt => {
    let value = content,
      selStartPos = evt.currentTarget.selectionStart;

    console.log(evt.currentTarget);

    // handle 4-space indent on
    if (evt.key === "Tab") {
      value =
        value.substring(0, selStartPos) +
        " " +
        value.substring(selStartPos, value.length);
      evt.currentTarget.selectionStart = selStartPos + 3;
      evt.currentTarget.selectionEnd = selStartPos + 4;
      evt.preventDefault();

      setContent(value);
    }
  };

In a nutshell, whenever the user hits a key, we check to see if it's the tab key. If it is, we alter the current state value from our input and add in some spacing, updating the selection point of the cursor along the way. This almost makes it feel like a genuine code editor.

And that's it. All done. But wait, things are looking a bit weird.

Screen capture of a weird styling issue in Code Sandbox from our React syntax highlighter

Let's create some nice styles to join up the dots.

Adding the styles

For our styles, there's nothing too flash, but here they are:

/** ---------------------------- */
/** --- Code editor ------------ */
/** ---------------------------- */
.code-edit-container {
  position: relative;
  height: 500px;
  border: 1px solid hsl(0, 0%, 60%);
  background-color: hsl(212, 35%, 95%);
  margin: 1em 0;
}

.code-input,
.code-output {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  padding: 1rem;
  border: none;
  font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
  font-size: 0.8rem;
  background: transparent;
  white-space: pre-wrap;
  line-height: 1.5em;
  word-wrap: break-word;
  font-size: 1rem;
}

.code-input {
  opacity: 1;
  margin: 0;
  color: hsl(0, 0%, 40%);
  resize: none;
}

.code-output {
  pointer-events: none;
  z-index: 3;
  margin: 0;
  overflow-y: auto;
}

code {
  position: absolute;
  top: 0;
  left: 0;
  margin: 0;
  padding: 1rem;
  display: block;
  color: hsl(0, 0%, 40%);
  font-size: 0.8rem;
  font-family: "PT Mono", monospace;
}

/* overrides */
.code-edit-container :not(pre) > code[class*="language-"],
.code-edit-container pre[class*="language-"] {
  background: transparent;
  margin: 0;
}

The main take away is that we create comparative text styling (font size, line-heights, etc.) between the text area input and the code output, and then layer the Prism-styled output over the text area input.

Finally, we have to add a few Prism overrides to just neaten everything up.

React code syntax highlighter in action

Helpful links

And that's it really. If you'd like to see it in action, there's a Code Sandbox below as well as some other helpful links.

Posted on by:

kendalmintcode profile

Rob Kendal {{☕}}

@kendalmintcode

Freelance front end developer, podcast host, and coding mentor.

Discussion

pic
Editor guide
 

Building the React code syntax highlighter from scratch

and the first step is installing code highlighter. I expected implementing actual code highlighting.

 

Sorry that your expectations weren't met on this occassion Lukas. However, since you have such a powerful tool like Prism, it doesn't make sense to recreate that. The disadvantage is that Prism only highlights static bodies of code. It doesn't dynamically handle changes and you certainly can't have it work like a code editor with highlighting, which is what this article is about; a simple, quick code editor with syntax highlighting. I'm sorry if you've been misled anywhere or given the wrong impression of what we were doing here.

 

I absolutely agree with you, just the "from scratch" hints it wont use any library.

Tell you what sir, because I like your moxy, I'm going to change that header and prevent future confusion :D

 

Thanks for the post, Rob.

Can you syntax-highlight code snippets? :)
Reference: The Editor Guide.

Probably more so for this post~

 

Thanks Sung. D’you know this is automatically pulled in from my blog and even though that has the correct markdown for code snippets, it’s been missed here! Oh the irony!!

However, rest assured I’ve updated the post and will keep my peepers out for this happening with future posts!!

Good spot 👍🏻😎