apocalypse

dev

How to paste image to input field and process it with React

#Motivation

In HTML there is a native file input (<input type="file" />), that allow you to select a file from your device in order to save it to some external storage, and then display it somewhere in the app. That's fine, but in order for this to work, you need to have a physical file on your hard drive. This may be a small inconvenience or a big one, depending on your use case.

#Alternatives to file input

Let's say, that you don't want to save an image on your hard drive prior to uploading in to a website. What are the options? Well, the easiest one is to save the URL to the image instead. URLs are just a text in a specific format, so they would take very little space in your database. There are many issues with that approach, though. URLs are really volatile - you can fetch some resource from them one day, and the other day they get removed, moved to some other URL, auth-protected, you name it. The next downside is that you can only save the images that are already uploaded to some other server. What if you want to save a screenshot from your system's clipping tool? Yeah, bummer.

It doesn't have to be like this. In this post, I'd like to show you, how to implement pasting an image from your clipboard straight to your app. No need to have the file on your hard drive, nor some other existing URL. Everything will be done using the ClipboardEvent, that has a pretty solid support in most browsers.

#Implementation

We will start with a blank React component, that looks like this:

import { ClipboardEventHandler, useState } from 'react'; function PasteImageInput() { // the state isn't necessary, but we're gonna need it to generate preview const [pastedFile, setPastedFile] = useState<File | null>(null); const pasteHandler: ClipboardEventHandler = async (e) => { // most of our logic will live here }; return ( <input name='main-image' label='Main image' placeholder='paste image here ;))' onPaste={pasteHandler} /> ); } export default PasteImageInput;

As mentioned in the snippet, most of the job needs to be done in this pasteHandler function. The name is up to you, but it will be benefitial to type it properly, to get the much-appreciated autocompletion. For this to work in TS, you need to type the whole function as ClipboardEventHandler imported from React. The native (e: ClipboardEvent) won't work.

import { ClipboardEventHandler } from 'react'; ... const pasteHandler: ClipboardEventHandler = async (e) => {}

#The pasteHandler function

Our handler would probably look something like this:

const pasteHandler: ClipboardEventHandler = async (e) => { e.preventDefault(); const files = Array.from(e.clipboardData?.files ?? []); const images = files.filter((file) => file.type.includes('image')); if (!images.length) { return; } const blob = images[0]; setPastedFile(blob); try { // Send blob to the server } catch (e) { console.log(e); } };

Let's break it down line by line.

e.preventDefault();

We prevent the default behavior, in this case pasting content into the input area. It's not necessary, but we probably don't want any junk in the input field, so we might as well add this line. If you want to implement this feature in a textarea where user can enter some other content, then don't add it ;).

const files = Array.from(e.clipboardData?.files ?? []);

This is probably the most important line for this function. ClipboardEvent, regardless of the element it was fired on, contains the clipboardData object, of DataTransfer type. There are two fields, that are important for us in this object - the clipboardData.items and clipboardData.files.

In our particular case, we can use either one of them to extract the pasted image. You may guess the difference just by their names. items contains every - guess what - item, pasted into the field. That could be either text or file. files contains only the latter. As we only need the image, which is a file, we could use the files field to simplify the code a little. That's how it would look like if we used items instead:

const items = Array.from(e.clipboardData?.items ?? []); const images = items.filter((item) => item.type.includes('image')); if (!images.length) { return; } const blob = images[0].getAsFile(); // extra step - call getAsFile() if (!blob) { return; // extra step - make sure getAsFile() didn't return null }

Regardless of what field we use, we still have to wrap it with Array.from(), because neither of those fields is a regular Array and we would want it to be, in order to perform another step, which is:

const images = files.filter((file) => file.type.includes('image'));

In this step we filter out every file that is not an image. You could also be more strict here, eg. accept only pngs (image/png) or only webp (image/webp). Up to you.

if (!images.length) { return; } - classic short-circuit in case there was no image in pasted content.

const blob = images[0];

It's a rare case to paste multiple images at once and we only probably want to process one image anyway, so we can just take the first item from the array. It will be defined, because if it wasn't, the function would have returned earlier.

setPastedFile(blob);

We update the state defined earlier. Why? Imagine you want to preview the file pasted in the input and/or display its size. Now you just add this condition to your JSX and v'oila:

{pastedFile && ( <> <img src={URL.createObjectURL(pastedFile)} alt='preview' /> <p>size: {pastedFile.size}B</p> {/* not a bad idea to format size to more human-readable format */} </> )}

What's left is the logic to upload the image file to the server. There are many options available, I will cover some of them in the next post ;).

#Complete code snippet

import { ClipboardEventHandler, useState } from 'react'; function PasteImageInput() { const [previewFile, setPreviewFile] = useState<Blob | null>(null); const pasteHandler: ClipboardEventHandler = async (e) => { e.preventDefault(); const files = Array.from(e.clipboardData?.files ?? []); const images = files.filter((file) => file.type.includes('image')); if (!images.length) { return; } const blob = images[0]; setPreviewFile(blob); try { // Send blob to the server } catch (e) { console.log(e); } }; return ( <> <input name='main-image' label='Main image' placeholder='paste image here ;))' onPaste={pasteHandler} /> {previewFile && ( <> <img src={URL.createObjectURL(previewFile)} alt='preview' /> <p>size: {previewFile.size}</p> </> )} </> ); } export default PasteImageInput;

#Final notes

This was the most basic example for pasting an image to the input field and process it with React. Reality can be more complicated, so I'd like to finish this post with a few final notes, touching possible drawbacks.

#Compatibility

If you're concerned about your users not being able to use this feature due to a browser incompability, you can test if their browser supports ClipboardEvent by running a very simple if-statement:

if (!window.ClipboardEvent) { // logic to disable the input and display some message // or replace it with a classic file input instead } // alternatively: if (!('ClipboardEvent' in window)) { ... }

Related tags:

paste
image
clipboard
file
form
react
ClipboardEvent
from
ClipboardEventHandler
blob
clipboardData
createObjectURL