DEV Community

Eddyter
Eddyter

Posted on

How to Handle Image Uploads in a React Rich Text Editor (2026 Guide)

Complete guide to image uploads in a React rich text editor for 2026 — drag-and-drop, paste, storage backends, code samples for both fast and custom paths.

How to Handle Image Uploads in a React Rich Text Editor (2026 Guide)

Setting up image uploads in a React rich text editor is one of those tasks that looks simple but quickly turns complex. You need drag-and-drop, paste-from-clipboard, file picker, storage backend, resizing, alt text, and proper HTML output — all working smoothly together. Most React editor tutorials skip these details.
This guide walks you through the full image upload setup for a React rich text editor in 2026. You'll learn the architecture, the backend pattern, the frontend integration, and how to handle the tricky edge cases. By the end, you'll have a working image upload flow that's production-ready.
Most developers can ship image uploads in a React editor in under an hour with the right approach. Let's break down exactly how.
🎥 New to modern React editors? Watch: What is Eddyter? Why Developers Are Switching to This AI Editor (2026)

Why Image Upload Setup Is Harder Than It Looks

Before diving in, here's why React editor image uploads cause so much pain:
1. Multiple Entry Points
Users want to add images three ways: drag-and-drop, paste from clipboard, and file picker. Each has different event handlers.
2. Storage Backend Decisions
Where do images live? Your server? AWS S3? Cloudinary? Each has different APIs and security models.
3. Async Upload UX
Image uploads take time. Users need progress indicators, error handling, and the ability to keep typing while upload happens.
4. Resizing and Optimization
Raw camera images are huge. Auto-resizing and compression matter for performance.
5. Alt Text and Accessibility
Every image needs alt text. Many editors skip this entirely.
6. HTML Output Structure
The final HTML needs proper tags with src, alt, and ideally width and height attributes.
Doing this from scratch takes 1-2 weeks of engineering. Doing it with a modern editor takes under an hour.

The Modern Approach: Don't Build It From Scratch

The first thing to know: most React editor image upload solutions today don't require building the flow from scratch. Modern editors like Eddyter ship with drag-and-drop, paste, file picker, resizing, and alt text all built in.
If you're starting fresh in 2026, the fastest path is a modern editor with image uploads already solved. For broader context on why building from scratch rarely makes sense, see our Why Building Your Own Editor Is a Startup Killer post.
That said, this guide covers both approaches:
Option A: Image uploads with a modern editor (Eddyter) — 10 minutes
Option B: Custom image uploads on Lexical, TipTap, or other frameworks — 1-2 weeks
Let's start with the fast path.
Option A: Image Uploads with Eddyter (10 Minutes)
Eddyter is a modern React-rich text editor built on Meta's Lexical framework. Image upload handling — drag-and-drop, paste, file picker, resize handles, and alt text — is built in. No custom flow needed.
Step 1: Get Your API Key
Sign up at eddyter.com and grab your API key from eddyter.com/user/license-key. Add it to your environment variables:
bash

.env.local

NEXT_PUBLIC_EDDYTER_API_KEY=your-api-key
Step 2: Install Eddyter
bash
npm install eddyter
Step 3: Render the Editor
jsx
'use client';
import {
ConfigurableEditorWithAuth,
EditorProvider
} from 'eddyter';
import 'eddyter/style.css';
export default function Editor() {
const apiKey = process.env.NEXT_PUBLIC_EDDYTER_API_KEY || 'your-api-key';
const handleContentChange = (html: string) => {
console.log('Editor content:', html);
// Save to state, database, etc.
};
return (

apiKey={apiKey}
onChange={handleContentChange}
/>

);
}
That's it. Drag an image into the editor. Paste an image from clipboard. Or use the toolbar to pick a file. All three work out of the box.
What You Get For Free
When users add images to an Eddyter editor:

  • ✅ Drag-and-drop from desktop to editor
  • ✅ Paste from clipboard (screenshots, copied images)
  • ✅ File picker from the toolbar
  • ✅ Resize handles to scale images visually
  • ✅ Alt text prompts for accessibility
  • ✅ Automatic upload progress indicators
  • ✅ Managed storage included on Premium plans
  • ✅ Image optimization for web delivery
  • ✅ Mobile support (camera roll, touch gestures) For advanced image configuration, see the Eddyter documentation. 🎥 See it integrated in real time: Integrate Eddyter in 30 Minutes Using AI Tools — Cursor, Claude, Lovable For a complete Next.js walkthrough with image handling, see our How to Add a Rich Text Editor in Next.js tutorial. Option B: Building Custom Image Uploads (1-2 Weeks) If you've decided to build image uploads from scratch on Lexical, TipTap, or another framework, here's the full pattern. Be ready for significant engineering time. The Architecture You Need
  1. Custom image upload in a React editor needs four layers:
  2. Frontend handlers for drag, paste, and file picker events
  3. Upload service that sends files to your storage backend
  4. Storage backend (S3, Cloudinary, your own server)
  5. Editor integration that inserts the uploaded image URL Let's build each layer. Layer 1: Storage Backend Pick a storage solution. Common choices: AWS S3 (Best for Most Apps) Pros: Cheap, scalable, mature, great CDN integration with CloudFront. Cons: Setup complexity, requires AWS account. Cost: ~$0.023 per GB/month plus minimal request fees. Cloudinary (Best for Image Optimization) Pros: Built-in transformations, automatic format conversion, easy API. Cons: More expensive at scale. Cost: Free tier available, paid plans start at $99/month. Your Own Server

Pros: Full control, no third-party costs. Cons: You manage storage, scaling, CDN, backups. Cost: Server costs vary.
Vercel Blob Storage (Best for Vercel Apps)
Pros: Native Vercel integration, simple API. Cons: Vercel-only, costs scale with usage. Cost: Free tier, then usage-based.
For most teams, AWS S3 with a CloudFront CDN is the sweet spot. Here's the setup.
Layer 2: Backend Upload Endpoint
Create a Next.js Route Handler at app/api/upload-image/route.ts:
typescript

import { NextRequest, NextResponse } from 'next/server';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { v4 as uuidv4 } from 'uuid';
const s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
);
}
// Validate file size (5MB limit)
if (file.size > 5 * 1024 * 1024) {
return NextResponse.json(
{ error: 'File too large (max 5MB)' },
{ status: 400 }
);
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
return NextResponse.json(
{ error: 'Invalid file type' },
{ status: 400 }
);
}
// Generate unique filename
const extension = file.name.split('.').pop();
const filename = ${uuidv4()}.${extension};
// Convert to buffer
const buffer = Buffer.from(await file.arrayBuffer());
// Upload to S3
await s3.send(
new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: editor-uploads/${filename},
Body: buffer,
ContentType: file.type,
})
);
// Return the public URL
const url = https://${process.env.AWS_S3_BUCKET}.s3.amazonaws.com/editor-uploads/${filename};
return NextResponse.json({ url });
} catch (error) {
console.error('Upload error:', error);
return NextResponse.json(
{ error: 'Upload failed' },
{ status: 500 }
);
}
}
This handles file validation, S3 upload, and returns a public URL.

Layer 3: Frontend Upload Service

Create a helper that handles the upload:
typescript
// lib/uploadImage.ts
export async function uploadImage(file: File): Promise {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload-image', {
method: 'POST',
body: formData,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Upload failed');
}
const { url } = await response.json();
return url;
}

Layer 4: Editor Integration

Now wire it into your React editor. The code differs by framework. Here's the pattern for a basic React rich text editor:
jsx
'use client';
import { useCallback } from 'react';
import { uploadImage } from '@/lib/uploadImage';
export default function CustomEditor() {
const [content, setContent] = useState('');
const [uploading, setUploading] = useState(false);
// Handle drag-and-drop
const handleDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault();
const files = Array.from(e.dataTransfer.files);
await handleFiles(files);
}, []);
// Handle paste
const handlePaste = useCallback(async (e: React.ClipboardEvent) => {
const items = Array.from(e.clipboardData.items);
const imageFiles = items
.filter(item => item.type.startsWith('image/'))
.map(item => item.getAsFile())
.filter(Boolean) as File[];
if (imageFiles.length > 0) {
e.preventDefault();
await handleFiles(imageFiles);
}
}, []);
// Handle file picker
const handleFilePicker = useCallback(async () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.multiple = true;
input.onchange = async (e) => {
const files = Array.from((e.target as HTMLInputElement).files || []);
await handleFiles(files);
};
input.click();
}, []);
// Upload files and insert into editor
const handleFiles = async (files: File[]) => {
setUploading(true);
try {
for (const file of files) {
const url = await uploadImage(file);
insertImageIntoEditor(url, file.name);
}
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed. Please try again.');
} finally {
setUploading(false);
}
};
const insertImageIntoEditor = (url: string, filename: string) => {
const imgHtml = <img src="${url}" alt="${filename}" />;
setContent(prev => prev + imgHtml);
};
return (

  onDrop={handleDrop}<br>
  onDragOver={(e) =&gt; e.preventDefault()}<br>
  onPaste={handlePaste}<br>
&gt;<br>
  Insert Image<br>
  {uploading &amp;&amp; <p>Uploading...</p>}<br>

    contentEditable<br>
    dangerouslySetInnerHTML={{ __html: content }}<br>
  /&gt;<br>
<br>
Enter fullscreen mode Exit fullscreen mode

);

}

This is the basic pattern. Real implementations need much more — proper editor framework integration (Lexical or ProseMirror), resize handles, alt text prompts, error states, and mobile support.

Common Pitfalls in Custom Image Upload
Building image uploads from scratch surfaces tricky edge cases. Watch for these:

  1. CORS Errors
    S3 uploads fail when CORS isn't configured. Add a CORS policy to your S3 bucket allowing your domain:
    json
    [
    {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "POST", "PUT"],
    "AllowedOrigins": ["https://yourapp.com"],
    "ExposeHeaders": []
    }
    ]

  2. Large File Crashes

Mobile camera photos can be 5-15MB. Reject files over your limit early and tell the user clearly.

  1. Slow Uploads Block UI

Upload async and let users keep typing. Show a progress indicator on the image placeholder, not the whole editor.

  1. Pasted Images Lose Filenames

Pasted images often have generic names like image.png. Generate UUIDs server-side and let users add alt text manually.

  1. Image Optimization Skipped

Raw uploads are huge. Resize and compress server-side or use a service like Cloudinary that does this automatically.

  1. Memory Leaks With Object URLs

If you use URL.createObjectURL() for previews, remember to call URL.revokeObjectURL() after upload completes.

  1. No Retry on Failed Uploads

Networks fail. Add automatic retry with exponential backoff for upload failures.

  1. Mobile Quirks

iOS Safari has unique paste behavior. Test on real devices, not just desktop browsers.

Comparing Image Upload Approaches

For most teams, Eddyter saves 80-120 hours of engineering time. For broader editor comparisons, see 9 Best Rich Text Editors of 2026.

When Custom Image Upload Makes Sense
Building image uploads from scratch makes sense in these cases:

  1. The Editor IS Your Product

If you're building Notion or Linear, image upload UX is core to your differentiation. Build it.

  1. You Have Unique Compliance Requirements

Some industries need specific image handling (HIPAA, GDPR with regional storage). Custom builds give you full control.

  1. You're Already on Lexical/TipTap Production

If you've already invested heavily in a custom Lexical or TipTap setup, adding image upload is incremental work.

  1. You Need Highly Custom UI

If your image upload UI needs to look very different from defaults (e.g., a custom gallery picker), custom is the path.
For 99% of teams, none of these apply. The right call is a modern editor like Eddyter with image uploads already solved. For strategic context, see our Modern WYSIWYG Editor guide.

Best Practices for Image Uploads in 2026

Whether you build custom or use Eddyter, follow these practices:

  1. Always Validate File Size and Type

Reject files over 5MB and non-image MIME types early. Show clear error messages.

  1. Generate Unique Filenames Server-Side

Don't trust user-provided filenames. Use UUIDs to prevent collisions.

  1. Use a CDN

Serving images directly from S3 is slow. Add CloudFront or similar CDN in front.

  1. Optimize for Web

Convert to WebP when possible. Compress aggressively. Generate multiple sizes for responsive images.

  1. Add Alt Text

Every image needs alt text for accessibility and SEO. Prompt users to add it.

  1. Lazy Load Images

Use loading="lazy" on tags to defer offscreen images.

  1. Set Width and Height Attributes

Include width and height in the HTML output to prevent Cumulative Layout Shift (CLS).

  1. Plan for Mobile

Test camera upload, paste behavior, and touch resizing on real iOS and Android devices.

Ready to Ship Image Uploads in Your React Editor?

Stop spending weeks building image upload flows. Drop Eddyter into your React app today and get drag-and-drop, paste, resize handles, and managed storage in 10 minutes.
👉 Try Eddyter free at eddyter.com 📚 Read the docs 🎥 Watch the intro video | Watch the 30-min integration guide

Top comments (0)