Uploading Images to Server with React Quill

Quill is a ver powerful WYSIWYG text editor for web. For a project of mine I decided upon Quill to use and I needed image upload for it but like most text editor selected images are encoded in base64 and embedded into the article body. I had to mash together few things until it worked with NextJS and I will explain it at least show some code for it.

To integrate with NextJS I use React Quill package. You can install it by yarn add react-quill.

NextJS includes a very powerfull server side rendering capabilities. Unluckily it does not work with Quill because it depends on a browser to exists. To solve this we will create a dynamic component for it like below

import dynamic from "next/dynamic";

const ReactQuill = dynamic(
  async () => {
    const { default: RQ } = await import("react-quill");
    return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;
  },
  {
    ssr: false,
  }
);
Importing react quill dynamically

We added few options here like ssr: false which prevents component render while it is rendered on the server. We will use forwardedRef to access current editor instance, turns out dynamic() does not forward refs.

I will be creating a small page to test it out.

import { useMemo, useRef } from "react";


const Editor = () => {
    const editorRef = useRef(null);
    const modules = useMemo(() => ({
        toolbar: {
            container: [
                ["image"],
            ],

            handlers: {
                image: imageHandler,
            },
        },
    }));

    return (
        <ReactQuill 
            modules={modules}
            forwardedRef={editorRef}
        />
    )
}

export default Editor;
editor.js

We will use useRef to keep track of current editor instance active on the DOM. This way we can manipulate it.

To create a toolbar we create the modules variable and wrap it in useMemo callback otherwise Quill will rerender on every render of the page and prevent flickering. I have added ["image"] for now.

Following code blocks need to be defined in the Editor function above unless stated otherwise.

...
    const imageHandler = (a) => {
        const input = document.createElement("input");
        input.setAttribute("type", "file");
        input.setAttribute("accept", "image/*");
        input.click();

        input.onchange = () => {
            const file = input.files[0];

            // file type is only image.
            if (/^image\//.test(file.type)) {
                saveToServer(file);
            } else {
                console.warn("You could only upload images.");
            }
        };
    };
  ...
editor.js

Every tima a user initiates a image add request this method will be called so we can create our own flow for it. In this instance we create a hidden file input which only accepts images and simulate a click on it so the browser can show a file picker to user.

Whenever the input encounters a new event we can check if a new file is ready and refer it to be uploaded to the server.

...
function saveToServer(file) {
    const fd = new FormData();
    fd.append("upload", file);

    const xhr = new XMLHttpRequest();
    xhr.open("POST", "/api/media", true);
    xhr.onload = () => {
        if (xhr.status === 201) {
            // this is callback data: url
            const url = JSON.parse(xhr.responseText).url;
            insertToEditor(url);
        }
    };
    xhr.send(fd);
}
...
editor.js

To upload image to the server we create a FormData and send request to our own API function so it can be transferred. After the image uploaded to server we retrieve our url to insert into document.

...
function insertToEditor(url) {
    editorRef.current.getEditor().insertEmbed(null, "image", url);
}
...
editor.js

We use our editorRef here to access the current editor instance and embed and image. Quill already knows how to deal with images it only needs url of it.

Of course another thing NextJS provides us is API routes. This routes are lambda functions if you deploy with vercel.

In this case I want to transfer uploaded file to S3 bucket. To accomplish this we need few extra packages.

    yarn add multer multer-s3 aws-sdk

To handle file streams we need multer. To store our images in AWS we need it's sdk. To connect above two packages we need multer-s3.

export const config = {
  api: {
    bodyParser: false, 
  },
};

For api routes NextJS will parse the request body into JSON which is what many would want. But while uploading an image we use streams. And to disable JSON parsing excerpt above needs to be added.

import * as multer from "multer";
import * as AWS from "aws-sdk";
import * as multerS3 from "multer-s3";

const AWS_S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME;
const s3 = new AWS.S3();

AWS.config.update({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID_PER,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY_PER,
});

const upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: AWS_S3_BUCKET_NAME,
    acl: "public-read",
    key: function (request, file, cb) {
      cb(null, `${Date.now().toString()} - ${file.originalname}`);
    },
  }),
}).array("upload", 1);

export default handle(async (req, res) => {
  upload(req, res, function (error) {
    if (error) {
      return res.status(404);
    }
    res.status(201).json({ url: req.files[0].location });
  });
});

export const config = {
  api: {
    bodyParser: false, 
  },
};
/api/media.js

upload method will parse the incoming request find the file and redirect it into a S3 bucket, only thing we need to do is whether or not there was an error and respond with the URL of the newly uploaded image file.


797 words

2021-04-19