In today’s post, we’ll dive deep into integrating a popular React Markdown editor, react-simplemde-editor, with a custom backend for image uploads.

Introduction

The react-simplemde-editor provides a user-friendly interface for content creation in the Markdown format. By default, it supports a variety of features, but one aspect we’d like to customize is the image upload functionality. We want to leverage our own backend to store and serve the images.

The Component: MarkdownEditor

Let’s start by examining the provided React component, MarkdownEditor:

import React, { useRef, useCallback } from "react";
import EasyMDE from "easymde";
import "easymde/dist/easymde.min.css";
import ReactDOMServer from "react-dom/server";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { SimpleMdeReact } from "react-simplemde-editor";

Here, we import the necessary packages, including the editor itself, markdown rendering utilities, and styles.

Props and States

The MarkdownEditor component accepts two props: value and onChange.

interface MarkdownEditorProps {
  value: string;
  onChange: (value: string) => void;
}
  • value: Represents the current Markdown content.
  • onChange: A callback to inform parent components about content changes.

Editor Configuration

Next, the component sets up the editor configuration:

const releaseNote = useRef<string>("");

This useRef is utilized to store the current content, enabling efficient updates without unnecessary re-renders.

The primary configuration object for the editor is options. Within it, we define:

  • The markdown formatting styles for different elements.
  • The toolbar items.
  • A custom image upload function that interfaces with our backend.
  • Other aesthetic configurations.

Custom Image Upload

A key part of this setup is the imageUploadFunction:

imageUploadFunction(file, onSuccess, onError) {
  const formData = new FormData();
  formData.append("data", file);
  fetch("/...yoururl.../imageupload", {
    method: "POST",
    body: formData,
  })
  .then(response => response.json())
  .then(data => {
    if (data.url) {
      onSuccess(data.url);
    } else {
      onError("Error: No URL returned from server.");
    }
  })
  .catch(error => {
    onError("Error uploading image: " + error.toString());
  });
},

Whenever a user tries to upload an image, this function is triggered. It sends the image to our backend and then either succeeds (providing the URL of the uploaded image) or fails (with an error message).

Rendering the Preview

Another interesting feature is the custom preview rendering. Instead of relying on the default markdown rendering, we leverage ReactDOMServer and ReactMarkdown for enhanced rendering capabilities.

previewRender: () =>
  ReactDOMServer.renderToString(
    <ReactMarkdown className="table-auto" remarkPlugins={[remarkGfm]}>
      {releaseNote.current}
    </ReactMarkdown>
  ),

With this, our Markdown content supports GitHub-flavored markdown, thanks to remarkGfm.

Wrapping Up the Component

Lastly, the component returns the actual editor:

return (
  <SimpleMdeReact
    value={value}
    onChange={handleContentChange}
    options={options}
  />
);

The handleContentChange function simply updates our releaseNote ref and calls the passed-in onChange function.

Complete Code

import React, { useRef, useCallback } from "react";
import EasyMDE from "easymde";
import "easymde/dist/easymde.min.css";
import ReactDOMServer from "react-dom/server";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { SimpleMdeReact } from "react-simplemde-editor";
interface MarkdownEditorProps {
  value: string;
  onChange: (value: string) => void;
}
export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
  value,
  onChange,
}) => {
  const releaseNote = useRef<string>("");
  const options = {
    blockStyles: {
      code: "```",
    },
    toolbar: [
      "bold",
      "italic",
      "heading",
      "heading-1",
      "heading-2",
      "code",
      "quote",
      "unordered-list",
      "ordered-list",
      "link",
      "upload-image",
      "table",
      "horizontal-rule",
      "preview",
      "guide",
      "redo",
      "undo",
      "side-by-side",
    ],
    imageUploadFunction(file, onSuccess, onError) {
      const formData = new FormData();
      formData.append("data", file);
      fetch("/...yoururl.../imageupload", {
        method: "POST",
        body: formData,
      })
        .then((response) => response.json())
        .then((data) => {
          if (data.url) {
            onSuccess(data.url);
          } else {
            onError("Error: No URL returned from server.");
          }
        })
        .catch((error) => {
          onError("Error uploading image: " + error.toString());
        });
    },
    minHeight: "49vh",
    sideBySideFullscreen: false,
    previewRender: () =>
      ReactDOMServer.renderToString(
        <ReactMarkdown className="table-auto" remarkPlugins={[remarkGfm]}>
          {releaseNote.current}
        </ReactMarkdown>
      ),
    previewClass: "p-[20px]",
    borderColor: "rgba(255, 255, 255",
  } as EasyMDE.Options;
  const handleContentChange = useCallback(
    (value: string) => {
      releaseNote.current = value;
      onChange(value);
    },
    [onChange]
  );
  return (
    <SimpleMdeReact
      value={value}
      onChange={handleContentChange}
      options={options}
    />
  );
};