Creating a Rich Text Editor with TinyMCE and React
Tags: webdev, react, beginners, frontend
Published: 2023-07-02 00:14:21

Have you ever tried to build out a form that contains a field that requires a lot of text? For example a short bio for a user profile, or a cover letter field for a resume submission. You may want the text to be able to be styled and formatted as the user sees fit; multiple lines, paragraphs, perhaps even bullet points.

The first thing you'll notice is that, while you can format your text within the text field with multiple lines and paragraphs, the text is not rendered back in the same way it was formatted. The text is all together in a single line/paragraph.

Frustrating, isn't it? The reason for this is that when you submit text from a text area component, it is rendered in the DOM as a [plain text](https://www.geeksforgeeks.org/difference-between-rich-text-and-plain-text/) element. In order to preserve formatting, we need a submit as [rich text](https://www.geeksforgeeks.org/difference-between-rich-text-and-plain-text/#:~:text=styles%20and%20formatting.-,2.%20RICH%20TEXT%20%3A,-Rich%20text%20is), and in order to do that we need a [Rich Text Editor](https://en.wikipedia.org/wiki/Online_rich-text_editor).

Luckily, implementing a basic text editor in your React application is a fairly straightforward process. In this article I will show you how to implement a rich text editor using [TinyMCE](https://www.tiny.cloud/).

This article assumes you've created your React app via [create-react-app](https://create-react-app.dev/)

# Create a free TinyMCE account and receive an API key
Signup for a free TinyMCE account [here](https://www.tiny.cloud/auth/signup/). 

In order to use TinyMCE, you'll need an API key. Once you've signed up for an account, your API key will be displayed in your dashboard.

## Concealing your API key
If you're not familiar with how to [conceal](https://softwareengineering.stackexchange.com/questions/395128/why-must-api-keys-be-kept-private#:~:text=Why%3F,if%20they%20steal%20your%20credentials.) an API key, [dotenv](https://github.com/motdotla/dotenv) is the easiest and most popular solution. Follow the steps below:

- Install dotenv via npm:
`npm install dotenv --save`

- At the root of your app, create a file called `.env`
- Inside `.env`, save your API key in the following [format](https://jasonwatmore.com/post/2022/06/22/react-access-environment-variables-from-dotenv-env):
`REACT_APP_TINYMCE_API = <Your API key>`
- Go to your [.gitignore file](https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files) and insert the path to your .env file at the bottom. For example:

```
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

#api
.env
```

# Install and set up TinyMCE in your React project

In your React project install TinyMCE with the following command:
`npm install --save @tinymce/tinymce-react`

Once that's done, you'll need to set up your actual text editor component. In your `src` folder, create a file called `TextEditor.js` and insert the following code:

```javascript
import { useRef } from "react";
import { Editor } from "@tinymce/tinymce-react"

export default function TextEditor() {
    const TINY_MCE_API_KEY = process.env.REACT_APPS_TINY_MCE_API
    const editorRef = useRef(null);

    function log() {
        const content = editorRef.current.getContent();
        console.log(content)
    }
    
    return (
      <>
        <Editor
          apiKey={TINY_MCE_API_KEY}
          onInit={(evt, editor) => editorRef.current = editor}
          initialValue="<p>This is the initial content...</p>"
          onEditorChange={log}
          init={{
            height: 300,
            menubar: false,
            plugins: [
              'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
              'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
              'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
            ],
            toolbar: 'undo redo | blocks | ' +
              'bold italic forecolor | alignleft aligncenter ' +
              'alignright alignjustify | bullist numlist outdent indent | ' +
              'removeformat | help',
            content_style: 'body { font-family:Roboto,Helvetica,Arial,sans-serif; font-size:16px; line-height: 1; }'
          }}
        />
      </>
    );
  }

```
As you can see in the example code, the API key is accessed via `process.env.REACT_APPS_TINYMCE_API`. The content of the editor is being saved via the [useRef() hook](https://dmitripavlutin.com/react-useref/). We also have a function `log()` that will log the contents of the editor into the console via the `onEditorChange` prop.

Open up your console and start typing in the editor to see it in action:

![](https://i.imgur.com/NTfsjfI.gif)

As you may have already noticed, it's not just the text that's being logged into the console, but the corresponding HTML tags as well. This is important, as this will help us render the text properly in the end.

# Rendering the editor content using html-react-parser

If we try to render the content as is, this is what we'll see:

![](https://i.imgur.com/n9H8Hgj.gif)

Remember, even though the proper HTML tags are included. It's still just a regular string, so `<p>hello world</p>` is really `"<p>hello world</p>"`.

In order to properly display this content, we need to convert our HTML string into one or more React elements. There are several tools for this, but my personal favorite is [html-react-parser](https://www.npmjs.com/package/html-react-parser) for its ease of use.

## Install html-react-parser
`npm install html-react-parser --save`

## Import the module
```javascript
import parse from 'html-react-parser';
```
After that, it's just a matter of using the `parse()` function and passing an HTML string as an argument, for example `parse('<h1>single</h1>');` or `parse('<li>Item 1</li><li>Item 2</li>');`. In this case, we want to pass the content of the editor as the argument.

Let's refactor our code a little to better see it in action.

```javascript
import { useRef, useState } from "react";
import { Editor } from "@tinymce/tinymce-react"
import parse from 'html-react-parser';


export default function TextEditor() {
    const TINY_MCE_API_KEY = process.env.REACT_APPS_TINY_MCE_API
    const editorRef = useRef(null);
    const [textContent, setTextContent] = useState('')
    
    function log() {
        let content = editorRef.current.getContent();
        setTextContent(content)
    }

return (
      <>
        ...Editor component

        <button onClick={log}>Render Text</button>
        <br/>
        <div>
            {parse(textContent)}
        </div>
      </>
    );
  }
```

Now, we should be able to add rich text and have our styling and formatting preserved. Let's try copy/pasting some randomly generated text:

![](https://i.imgur.com/PXCgV0U.gif)

Much better!

# Resources
- https://www.geeksforgeeks.org/difference-between-rich-text-and-plain-text/
- https://dmitripavlutin.com/react-useref/
- https://softwareengineering.stackexchange.com/questions/395128/why-must-api-keys-be-kept-private#:~:text=Why%3F,if%20they%20steal%20your%20credentials.
- https://www.npmjs.com/package/html-react-parser