Base64 File Upload in React
Converting a file to Base64 in React uses the browser's FileReader API. You can then preview images inline, send the encoded data to an API, or store it locally. Here is everything you need.
- Convert a file to Base64 with FileReader.readAsDataURL
- Handle file input with onChange and useState
- Show an image preview with the Base64 data URI
- Extract just the Base64 data
- Send Base64 to a REST API with fetch
- TypeScript types for FileReader and Base64 handlers
- When to use Base64 vs multipart/form-data
- FAQ
Convert a file to Base64 with FileReader.readAsDataURL
The browser ships a built-in way to read files: the FileReader API. Its readAsDataURL method reads a File or Blob and produces a data URI — a string that begins with data:, declares the MIME type, and then contains the Base64-encoded bytes after ;base64,. There is no external library required; this works in every modern browser.
Reading is asynchronous. You start the read with readAsDataURL and listen for the load event, which fires once the file has been fully read into memory. The encoded result lives on reader.result (also available as event.target.result).
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result); // full data URI
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
// usage
const dataUri = await fileToBase64(myFile);
// "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."
Wrapping FileReader in a Promise (as above) lets you use async/await inside event handlers and effects, which reads far more cleanly than nesting callbacks.
Handle file input with onChange and useState
In React, you read the selected file from the onChange event of an <input type="file">. The chosen files live on event.target.files, which is a FileList. For a single-file input, grab the first entry. Store the resulting Base64 string in component state with useState so the UI re-renders.
import { useState } from "react";
function UploadForm() {
const [base64, setBase64] = useState("");
function handleChange(event) {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => setBase64(reader.result);
reader.readAsDataURL(file);
}
return (
<div>
<input type="file" accept="image/*" onChange={handleChange} />
{base64 && <p>Encoded {base64.length} characters</p>}
</div>
);
}
The accept attribute filters the native file picker — accept="image/*" shows only images, while accept=".pdf,.docx" restricts by extension. It is a hint, not a guarantee, so always validate the file type and size in your handler too.
Never call setBase64 directly with event.target.files[0] — that is a File object, not a string. The Base64 value is only available inside the asynchronous onload callback, after the read completes.
Show an image preview with the Base64 data URI
Because readAsDataURL returns a complete data URI, you can drop it straight into the src of an <img> element. The browser decodes and renders it locally — no network request, no temporary URL to clean up. This is the simplest possible image preview.
function ImagePreview() {
const [preview, setPreview] = useState("");
function handleChange(event) {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => setPreview(reader.result);
reader.readAsDataURL(file);
}
return (
<div>
<input type="file" accept="image/*" onChange={handleChange} />
{preview && (
<img
src={preview}
alt="preview"
style={{ maxWidth: 240, borderRadius: 8 }}
/>
)}
</div>
);
}
For large images, an alternative is URL.createObjectURL(file), which produces a short blob: URL instead of inlining the whole file. It uses far less memory for previews, but remember to call URL.revokeObjectURL when the component unmounts. Use the Base64 data URI when you also need the encoded string for an upload.
Extract just the Base64 data
A data URI bundles two things: metadata (data:image/png;base64,) and the raw Base64 payload. Many APIs want only the payload — the part after the comma. Splitting on the first comma gives you exactly that.
const dataUri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...";
const base64Only = dataUri.split(",")[1];
// "iVBORw0KGgoAAAANSUhEUg..."
// also capture the MIME type if your API needs it separately
const mimeType = dataUri.match(/^data:(.*?);/)?.[1];
// "image/png"
Keep the full data URI when the value will be rendered in the browser (an <img src>, a CSS background-image, or stored for later display). Strip the prefix when sending to a backend that expects the bare Base64 string and tracks the content type in a separate field.
Do not assume the prefix is always a fixed length. The MIME type varies (image/jpeg, application/pdf, etc.), so the prefix length differs. Always split on the comma rather than slicing a hard-coded number of characters.
Send Base64 to a REST API with fetch
If your endpoint accepts JSON, embed the Base64 string in the request body. This is convenient when you want to send the file alongside other structured fields in one payload.
async function uploadAsBase64(file) {
const reader = new FileReader();
const dataUri = await new Promise((resolve, reject) => {
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
const response = await fetch("/api/upload", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: file.name,
mimeType: file.type,
data: dataUri.split(",")[1], // strip the prefix
}),
});
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
return response.json();
}
On the server, decode the string back to bytes before writing it to disk or object storage. In Node.js that is one line: Buffer.from(data, "base64") yields the original binary buffer.
Base64 inflates payload size by roughly 33%. For multi-megabyte files this means slower uploads and higher memory use on both client and server. For anything large, prefer multipart/form-data (see the last section) or a presigned upload directly to storage.
TypeScript types for FileReader and Base64 handlers
TypeScript already ships DOM types for FileReader and React events, so you rarely need custom interfaces. The one gotcha is that reader.result has type string | ArrayBuffer | null — because FileReader can also read binary. After readAsDataURL the result is always a string, so narrow it.
import { useState, ChangeEvent } from "react";
function readAsDataURL(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
if (typeof result === "string") resolve(result);
else reject(new Error("Expected a data URL string"));
};
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
function Uploader() {
const [base64, setBase64] = useState<string>("");
async function handleChange(event: ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
if (!file) return;
setBase64(await readAsDataURL(file));
}
return <input type="file" onChange={handleChange} />;
}
Type the input handler with ChangeEvent<HTMLInputElement>, type state as useState<string>, and let the helper return a Promise<string>. With that, the rest of your component stays fully type-checked without any casts.
Avoid casting reader.result as string. The explicit typeof result === "string" check is safer — it catches the binary-read case at runtime instead of silently passing an ArrayBuffer downstream.
When to use Base64 vs multipart/form-data
The native way to upload a file from the browser is multipart/form-data. You build a FormData object, append the raw File, and let the browser stream the binary directly. No 33% overhead, no manual encoding, and the file uploads as a true binary part.
async function uploadMultipart(file: File) {
const form = new FormData();
form.append("file", file); // raw binary, no encoding
form.append("filename", file.name);
const response = await fetch("/api/upload", {
method: "POST",
body: form, // do NOT set Content-Type — the browser adds the boundary
});
return response.json();
}
Use the right tool for the job:
- Reach for multipart/form-data for most file uploads, especially large files. It is the efficient, standard approach and streams binary without bloat.
- Reach for Base64 when your API only accepts JSON, when you need to bundle the file inside a JSON object with other fields, when embedding small images as data URIs, or when storing a thumbnail in
localStorage.
When sending FormData with fetch, do not manually set the Content-Type header. The browser generates it automatically along with the required multipart boundary; setting it yourself breaks the request.
FAQ
How do I convert a file to Base64 in React?
Use FileReader: const reader = new FileReader(); reader.onload = (e) => setBase64(e.target.result); reader.readAsDataURL(file). The result is a data URI like data:image/png;base64,iVBOR... To get just the Base64 part, split on comma: e.target.result.split(",")[1]
How do I preview an image after Base64 encoding in React?
Store the full data URI (from FileReader.readAsDataURL) in state and use it as the src of an img element: <img src={base64DataUri} alt="preview" />. The browser renders it directly without any HTTP request.
Should I use Base64 or multipart/form-data for file uploads?
For most file uploads, multipart/form-data is more efficient — it avoids the 33% Base64 overhead and is the native browser standard. Use Base64 when your API only accepts JSON, when you need to embed the file in a JSON payload alongside other data, or for small images used as data URIs.
Try Base64 encoding and decoding instantly
Paste any string or file — base64.dev auto-detects and converts it instantly.
Open base64.dev →