Пошук уроків, статей та іншого контенту
Реалізуйте вибір файлів, перевірку типу й розміру, попередній перегляд та надсилання через FormData.
Завантаження файлу у React зазвичай складається з таких кроків:
Користувач вибирає один або кілька файлів через <input type="file">.
Компонент отримує об’єкти File з event.target.files.
Файли перевіряються за типом і розміром.
Для зображень або PDF створюється попередній перегляд.
Файли додаються до FormData.
FormData надсилається на сервер через fetch.
Об’єкт File містить, зокрема:
name — назву файлу;
size — розмір у байтах;
type — MIME-тип, наприклад image/png;
lastModified — час останньої зміни.
Атрибут accept лише підказує браузеру, які файли бажано показувати у вікні вибору. Він не замінює перевірку у JavaScript і тим більше не замінює перевірку на сервері.
У прикладі нижче дозволено вибирати зображення JPEG і PNG, а також PDF-файли. Максимальний розмір одного файлу — 5 МБ.
import { useEffect, useRef, useState } from "react";
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const ALLOWED_TYPES = new Set([
"image/jpeg",
"image/png",
"application/pdf",
]);
function validateFile(file) {
if (!ALLOWED_TYPES.has(file.type)) {
return `${file.name}: дозволені лише JPEG, PNG та PDF`;
}
if (file.size > MAX_FILE_SIZE) {
return `${file.name}: розмір не може перевищувати 5 МБ`;
}
return null;
}
export default function FileUpload() {
const [files, setFiles] = useState([]);
const [errors, setErrors] = useState([]);
const [status, setStatus] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [previews, setPreviews] = useState([]);
const inputRef = useRef(null);
useEffect(() => {
const nextPreviews = files.map((file) => ({
file,
url: URL.createObjectURL(file),
}));
setPreviews(nextPreviews);
return () => {
nextPreviews.forEach(({ url }) => {
URL.revokeObjectURL(url);
});
};
}, [files]);
function handleFileChange(event) {
const selectedFiles = Array.from(event.target.files || []);
const nextFiles = [];
const nextErrors = [];
selectedFiles.forEach((file) => {
const error = validateFile(file);
if (error) {
nextErrors.push(error);
} else {
nextFiles.push(file);
}
});
setFiles(nextFiles);
setErrors(nextErrors);
setStatus("");
// Дозволяє повторно вибрати той самий файл після помилки або видалення.
event.target.value = "";
}
function removeFile(fileToRemove) {
setFiles((currentFiles) =>
currentFiles.filter((file) => file !== fileToRemove),
);
setStatus("");
}
async function handleSubmit(event) {
event.preventDefault();
if (files.length === 0) {
setStatus("Виберіть хоча б один файл.");
return;
}
const formData = new FormData();
files.forEach((file) => {
formData.append("files", file, file.name);
});
setIsSubmitting(true);
setStatus("");
try {
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error("Сервер не прийняв файли");
}
setStatus("Файли успішно надіслано.");
setFiles([]);
if (inputRef.current) {
inputRef.current.value = "";
}
} catch (error) {
setStatus(error.message || "Не вдалося надіслати файли.");
} finally {
setIsSubmitting(false);
}
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="file-input">Виберіть файли:</label>
<input
ref={inputRef}
id="file-input"
type="file"
accept="image/jpeg,image/png,application/pdf"
multiple
onChange={handleFileChange}
/>
{errors.length > 0 && (
<ul role="alert">
{errors.map((error) => (
<li key={error}>{error}</li>
))}
</ul>
)}
{previews.length > 0 && (
<section aria-label="Попередній перегляд">
<h2>Вибрані файли</h2>
<ul>
{previews.map(({ file, url }) => (
<li key={`${file.name}-${file.lastModified}`}>
<p>
<strong>{file.name}</strong>{" "}
({Math.round(file.size / 1024)} КБ)
</p>
{file.type.startsWith("image/") && (
<img
src={url}
alt={`Попередній перегляд ${file.name}`}
width="240"
/>
)}
{file.type === "application/pdf" && (
<iframe
src={url}
title={`Попередній перегляд ${file.name}`}
width="400"
height="300"
/>
)}
<button
type="button"
onClick={() => removeFile(file)}
>
Видалити
</button>
</li>
))}
</ul>
</section>
)}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Надсилання..." : "Надіслати"}
</button>
{status && <p role="status">{status}</p>}
</form>
);
}Для вибору файлів використовується звичайний HTML-елемент:
<input type="file" multiple onChange={handleFileChange} />Властивість multiple дозволяє вибрати кілька файлів. У onChange браузер передає подію, а список файлів доступний через:
event.target.filesevent.target.files має тип FileList, тому для використання методів масиву його зручно перетворити:
const selectedFiles = Array.from(event.target.files);Не можна встановити довільне значення для поля типу file. Браузер навмисно не дозволяє React або JavaScript автоматично вказати шлях до локального файлу користувача.
Тип файлу перевіряється через file.type:
file.type === "image/png";Розмір зберігається у байтах. Наприклад, обмеження у 5 МБ можна записати так:
const MAX_FILE_SIZE = 5 * 1024 * 1024;Функція validateFile у прикладі повертає текст помилки або null, якщо файл пройшов перевірку. Такий підхід дозволяє окремо перевірити кожен файл і не додавати некоректні файли до стану.
Важливо: значення file.type надходить від браузера і не є повністю надійним доказом вмісту файлу. Сервер повинен повторно перевіряти:
фактичний тип файлу;
розмір;
допустимі розширення;
вміст файлу;
правила доступу для поточного користувача.
Для попереднього перегляду локального файлу використовується:
const url = URL.createObjectURL(file);Цей метод створює тимчасову URL-адресу, яку можна передати в src для <img> або <iframe>.
Коли URL більше не потрібна, її слід звільнити:
URL.revokeObjectURL(url);У компоненті це робиться в очищенні useEffect. Без цього під час вибору великої кількості файлів або багаторазової заміни файлів пам’ять може використовуватися неефективно.
Для зображення підходить елемент img:
<img src={url} alt={file.name} />Для PDF можна використати iframe:
<iframe src={url} title={file.name} />Для інших типів файлів достатньо показати назву, розмір і кнопку видалення. Не кожен формат можна безпечно або коректно відобразити в браузері.
Файли не потрібно вручну перетворювати на JSON. Для multipart-запиту використовується FormData:
const formData = new FormData();
files.forEach((file) => {
formData.append("files", file, file.name);
});Після цього об’єкт передається до fetch:
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});Не встановлюйте заголовок Content-Type вручну:
// Не потрібно робити так:
headers: {
"Content-Type": "multipart/form-data",
}Браузер сам встановить правильний Content-Type разом із boundary — спеціальним роздільником частин multipart-запиту. Якщо встановити заголовок вручну, сервер може не розібрати тіло запиту.
Назва поля в formData.append повинна відповідати очікуванням сервера:
formData.append("files", file);Якщо сервер очікує окремі поля або масив із конкретною назвою, назву потрібно узгодити з API.
Під час надсилання варто блокувати кнопку:
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Надсилання..." : "Надіслати"}
</button>Це запобігає випадковому повторному надсиланню одного й того самого набору файлів.
Також компонент повинен обробляти різні результати запиту:
успішне надсилання;
помилку сервера;
мережеву помилку;
спробу надіслати порожній список файлів.
acceptaccept змінює список файлів у діалозі вибору, але не є повноцінною валідацією. Некоректний файл усе одно може потрапити до компонента або бути підроблений.
Перевіряйте файли у React для зручності користувача й обов’язково повторюйте перевірку на сервері.
Content-Type вручнуДля FormData браузер повинен сам сформувати заголовок із boundary. Ручне встановлення multipart/form-data часто призводить до помилок обробки запиту.
Кожен виклик URL.createObjectURL створює тимчасовий ресурс. Після видалення або заміни файлів викликайте URL.revokeObjectURL.
Об’єкт File не слід передавати через JSON.stringify. Для файлів використовуйте FormData.
Якщо користувач вибере той самий файл повторно, деякі браузери можуть не викликати change. Очищення event.target.value дозволяє коректно обробити повторний вибір.
Перед створенням запиту перевірте, що список файлів не порожній. Це також спрощує обробку помилок на сервері.
Для вибору файлів використовуйте <input type="file">.
Список файлів отримуйте з event.target.files і перетворюйте на масив.
Перевіряйте MIME-тип і розмір кожного файлу до надсилання.
accept є лише підказкою для браузера, а не захистом.
Для попереднього перегляду використовуйте URL.createObjectURL.
Після завершення роботи з попереднім переглядом викликайте URL.revokeObjectURL.
Для надсилання файлів використовуйте FormData.
Не встановлюйте Content-Type вручну для multipart-запиту.
Повторно перевіряйте тип, розмір і вміст файлів на сервері.