Tackle building a Rich Text Editor with Remirror and React
Tags: webdev, javascript, programming, tutorial
Published: 2023-10-10 15:40:09

Sweat. Blood. Tears. 

Staring at a screen for countless hours

Rich Text Editors are one of the most challenging aspects of frontend development

I pushed through the pain and built it so you won't have to

![Am fine btw](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d8sdtteunifbr8hlozp1.png)

Let's dive in

## What we're building

Sike.

Let's get a topological view of what you'll build first

You will build a `<ContentEditor/>` component that connects to a `useContentEditor` hook

The hook will allow getting and setting of the markdown content

![Component usage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ii57baidra89qkvo616o.png)

[Live Demo](https://replit.com/@IroncladDev/Rich-Text-Editor?v=1). Take it for a spin

Seems pretty straightforward, right?

Let's build.

## Environment Setup

Spin up a [React Repl](https://replit.com/@replit/React-TypeScript?v=1) on [Replit](https://replit.com).

Alternatively, create a new react app with `create-react-app`.

## The Component

Install `@remirror/react` and `remirror` via npm.

```
npm install remirror @remirror/react
```

Copy. Paste. [Docs](https://remirror.io/docs/) if you want

TL;DR, we're just importing and rendering the `Remirror` component from `@remirror/react`

The heavy lifting will be done by the `useContentEditor` hook

```tsx
import { EditorComponent, Remirror, ReactExtensions, UseRemirrorReturn } from "@remirror/react";
import { AnyExtension, RemirrorEventListener } from "remirror";

export function ContentEditor({
  editor: { manager, state },
  onChange,
}: {
  editor: UseRemirrorReturn<ReactExtensions<AnyExtension>>;
  onChange: RemirrorEventListener<AnyExtension>;
}) {
  return (
    <div>
      <Remirror
        manager={manager}
        state={state}
        onChange={onChange}
        autoFocus
      >
        <EditorComponent />
      </Remirror>
    </div>
  );
}
```

## Extensions

Editor functionality is based on [Remirror Extensions](https://remirror.io/docs/extensions/).

`BoldExtension` allows us to use bold text formatting

`ItalicExtension` likewise

Self-explanatory.

Create a file `extensions.ts` and paste the extension config for allowing most aspects of basic markdown syntax.

```ts
import { textblockTypeInputRule } from "prosemirror-inputrules";
import { AnyExtension, ExtensionPriority } from "remirror";
import {
  BlockquoteExtension,
  BoldExtension,
  BulletListExtension,
  CodeBlockExtension,
  CodeExtension,
  HardBreakExtension,
  HeadingExtension,
  ItalicExtension,
  LinkExtension,
  ListItemExtension,
  MarkdownExtension,
  OrderedListExtension,
  PlaceholderExtension,
  StrikeExtension,
  TextHighlightExtension,
  TrailingNodeExtension,
} from "remirror/extensions";
import languages from "./languages";

/**
 * Modifies the Code Block extension creation behavior. Creates a code block after language specification + space/line break.
 */
class ExtendedCodeBlockExtension extends CodeBlockExtension {
  createInputRules() {
    const regexp = /^```([a-zA-Z]+)?(\n|\s)/;
    return [textblockTypeInputRule(regexp, this.type)];
  }
}

/**
 * A list of allowed Remirror Extensions
 */
const extensions =
  (placeholder: string = "") =>
    (): Array<AnyExtension> => [
      new BoldExtension(),
      new ItalicExtension(),
      new HeadingExtension(),
      new CodeExtension(),
      new BlockquoteExtension(),
      new LinkExtension({ autoLink: true }),
      new StrikeExtension(),
      new BulletListExtension({ enableSpine: true }),
      new OrderedListExtension(),
      new ListItemExtension({
        priority: ExtensionPriority.High,
        enableCollapsible: true,
      }),
      new ExtendedCodeBlockExtension({
        supportedLanguages: languages,
      }),
      new TrailingNodeExtension(),
      new MarkdownExtension({
        copyAsMarkdown: false,
      }),
      new HardBreakExtension(),
      new TextHighlightExtension(),
      new PlaceholderExtension({
        placeholder,
      }),
    ];

export default extensions;
```

Why am I extending `CodeBlockExtension`?

The default config is less user-friendly and a bit whacky.

A little bit of placeholder drama, but it makes future configuration easy.

## Syntax Highlighting

This was half the headache for me.

Took forever to figure it out, but I did it.

Create a file `languages.ts` and paste the following.

```tsx
import clike from "refractor/lang/clike.js";
import go from "refractor/lang/go.js";
import java from "refractor/lang/java.js";
import kotlin from "refractor/lang/kotlin.js";
import md from "refractor/lang/markdown.js";
import php from "refractor/lang/php.js";
import python from "refractor/lang/python.js";
import ruby from "refractor/lang/ruby.js";
import rust from "refractor/lang/rust.js";
import swift from "refractor/lang/swift.js";
import typescript from "refractor/lang/typescript.js";
import html from "refractor/lang/xml-doc.js";

const languages = [
  typescript,
  md,
  python,
  html,
  clike,
  java,
  php,
  swift,
  kotlin,
  ruby,
  go,
  rust,
];

export default languages;
```

Straightforward.

`refractor` should have been pre-installed with remirror.

## The hook

This is where everything comes together.

Let's start with imports.

```ts
import remirrorExtensions from "./extensions";
import { ReactExtensions, useRemirror, UseRemirrorReturn } from "@remirror/react";
import { useCallback, useState } from "react";
import { AnyExtension, RemirrorEventListener, RemirrorEventListenerProps } from "remirror";
```

And I'm sorry for not separating type imports.

![Oh no, anyway](https://media.tenor.com/RL0-tTg3QOQAAAAC/oh-no.gif)

Next, return type.

```ts
export interface UseContentEditorReturnType {
  editor: UseRemirrorReturn<ReactExtensions<AnyExtension>>;
  onChange: RemirrorEventListener<AnyExtension>;
  content: string;
  setContent: (content: string) => void;
}
```

`editor`: Modified Remirror utility

`onChange`: Also Modified Remirror utility

`content`: markdown content

`setContent`: to control and set the editor content state.

Create the hook function.

```ts
export function useContentEditor(
  // Initial value
  value: string,
  // Optional arguments
  args?: {
    placeholder?: string;
  }
): UseContentEditorReturnType {

}
```

Red squiggly lines.

Endure it.

Create a useState hook for the markdown content

Load the extensions

Configure a new remirror editor instance

```ts
const [content, setMarkdownContent] = useState(value);

const extensions = remirrorExtensions(args?.placeholder);

const editor = useRemirror({
  extensions,
  stringHandler: "markdown",
  content: value,
  selection: "start",
});

const { onChange, manager } = editor;
```

Create a modified `onEditorChange` function to update the markdown string state

```
const onEditorChange = useCallback(
  (
    parameter: RemirrorEventListenerProps<
      ReactExtensions<ReturnType<typeof extensions>[number]>
    >,
  ) => {
    const markdownContent = parameter.helpers.getMarkdown(parameter.state);
    setMarkdownContent(markdownContent);
    onChange(parameter);
  },
  [onChange],
);
```

Create a `setContent` function that sets the markdown state and updates the editor

```ts
const setContent = useCallback(
  (value: string) => {
    manager.view.updateState(
      manager.createState({
        content: value,
        selection: "end",
        stringHandler: "markdown",
      }),
    );
    setMarkdownContent(value);
  },
  [manager],
);
```

Return.

```
return {
  editor,
  onChange: onEditorChange,
  setContent,
  content,
};
```

## Wrapping up

Let's put everything together.

A little different than the overview in the beginning.

Adjust import paths if necessary.

```tsx
import { ContentEditor } from './ContentEditor'
import { useContentEditor } from './ContentEditor/useContentEditor'

const initialValue = `This is a rich text editor demo.

It supports **bold**, _italic_, \`code\`, and ~~strikethrough~~ formatting.

> Blockquotes are supported as well

\`\`\`js
// And so are
console.log("Code blocks with syntax highlighting");
\`\`\`

- Lists
- Work
- As well

1. And numbered ones
2. Behave similarly`

export default function App() {
  const { editor, onChange, content, setContent } = useContentEditor(initialValue);
  
  return (
    <div className="container">
      <h1>Rich text editor demo</h1>

      <div>
        <button onClick={() => setContent(initialValue)}>Reset</button>
      </div>
      <div className="content">
        <ContentEditor editor={editor} onChange={onChange} />

        <pre className="markdown-content">{content}</pre>
      </div>
    </div>
  )
}
```

I forgot the CSS.

That's yours to take care of

Or go ahead and [use mine](https://replit.com/@IroncladDev/Rich-Text-Editor?v=1#src/App.css)

We're done

You've just built on of the most challenging things in the entirety of web development.

---

Let's get in touch.

- [Website](https://connerow.dev)
- [X / Twitter](https://x.com/IroncladDev)
- [Github](https://github.com/IroncladDev)