Создание пользовательского компонента загрузки файлов для React
Нативный file input работает, но выглядит непривлекательно, негибок и не предоставляет никакой обратной связи во время загрузки. Большинство разработчиков используют библиотеки вроде react-dropzone для решения этой проблемы. Но если вы хотите полного контроля — или просто меньше зависимостей — создание пользовательского компонента загрузки файлов для React не так сложно, если понимать ограничения.
Это руководство охватывает основы: доступный выбор файлов, опциональный функционал drag-and-drop, валидацию, предпросмотр, отслеживание прогресса и отмену загрузки. Всё с использованием современных функциональных компонентов и хуков.
Ключевые выводы
- File inputs по своей природе являются неконтролируемыми в React из-за ограничений безопасности браузера, которые предотвращают программное присвоение значений
- Всегда сочетайте клиентскую валидацию с серверной, поскольку MIME-типы и атрибут
acceptможно обойти - Используйте
XMLHttpRequestвместоfetchдля надёжного отслеживания прогресса загрузки - Отзывайте object URL, созданные для предпросмотра файлов, чтобы предотвратить утечки памяти
- Drag-and-drop должен быть прогрессивным улучшением, а не заменой взаимодействия с клавиатурой и кликами
Почему file inputs остаются неконтролируемыми
Вот что сбивает с толку разработчиков React: вы не можете программно установить значение <input type="file">. Безопасность браузера это предотвращает. Это означает, что file inputs по своей природе неконтролируемые — React может читать из них, но никогда не может записывать в них.
Это важно и для Form Actions в React 19. Хотя useFormStatus может отслеживать состояние ожидания, Server Actions не предназначены для загрузки больших файлов. Вы всё равно будете обрабатывать файлы на клиентской стороне перед отправкой куда-либо.
Основа: доступный выбор файлов
Начните с самого input. Скройте его визуально, но сохраните доступность:
const FileUploader = () => {
const inputRef = useRef<HTMLInputElement>(null)
const [files, setFiles] = useState<File[]>([])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFiles(Array.from(e.target.files))
}
}
return (
<div>
<input
ref={inputRef}
type="file"
id="file-upload"
className="sr-only"
onChange={handleChange}
multiple
aria-describedby="file-upload-help"
/>
<label htmlFor="file-upload" className="upload-button">
Select files
</label>
<span id="file-upload-help" className="sr-only">
Accepted formats: images, PDFs up to 10MB
</span>
</div>
)
}
Атрибут accept носит только рекомендательный характер — пользователи могут его обойти. Всегда проводите валидацию на сервере.
Добавление drag-and-drop как прогрессивного улучшения
Функционал drag-and-drop улучшает пользовательский опыт, но не является обязательным. Оставьте взаимодействие с клавиатурой и кликами в качестве основного пути:
const [isDragging, setIsDragging] = useState(false)
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(true)
}
const handleDragLeave = () => setIsDragging(false)
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const droppedFiles = Array.from(e.dataTransfer.files)
setFiles(droppedFiles)
}
Примечание: DataTransferItem.getAsFileSystemHandle() предоставляет доступ к директориям в современных браузерах, но поддержка остаётся ограниченной. Придерживайтесь dataTransfer.files для широкой совместимости.
Клиентская валидация
Проводите валидацию перед загрузкой для улучшения пользовательского опыта:
const validateFile = (file: File): string | null => {
const maxSize = 10 * 1024 * 1024 // 10MB
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
if (file.size > maxSize) return 'File exceeds 10MB limit'
if (!allowedTypes.includes(file.type)) return 'Invalid file type'
return null
}
Помните: MIME-типы можно подделать. Серверная валидация обязательна.
Discover how at OpenReplay.com.
Предпросмотр файлов с правильной очисткой
Для предпросмотра изображений URL.createObjectURL создаёт временные URL. Отзывайте их, чтобы предотвратить утечки памяти:
const [preview, setPreview] = useState<string | null>(null)
useEffect(() => {
if (files[0]?.type.startsWith('image/')) {
const url = URL.createObjectURL(files[0])
setPreview(url)
return () => URL.revokeObjectURL(url)
}
}, [files])
Отслеживание прогресса и отмена загрузки
Вот неудобная правда: fetch не поддерживает надёжное отслеживание прогресса загрузки. Для реального отслеживания прогресса используйте XMLHttpRequest:
const [progress, setProgress] = useState(0)
const xhrRef = useRef<XMLHttpRequest | null>(null)
const upload = (file: File) => {
const xhr = new XMLHttpRequest()
xhrRef.current = xhr
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
setProgress(Math.round((e.loaded / e.total) * 100))
}
}
const formData = new FormData()
formData.append('file', file)
xhr.open('POST', '/api/upload')
xhr.send(formData)
}
const cancel = () => xhrRef.current?.abort()
Для больших файлов рассмотрите прямую загрузку в объектное хранилище (S3 presigned URLs) или подходы с разбиением на части/multipart. Отправка многогигабайтных файлов через сервер приложения редко заканчивается хорошо.
Состояния ошибок
Отслеживайте состояние загрузки явно:
type UploadState =
| { status: 'idle' }
| { status: 'uploading'; progress: number }
| { status: 'success'; url: string }
| { status: 'error'; message: string }
Это дискриминируемое объединение делает рендеринг простым и типобезопасным.
Когда использовать библиотеку вместо собственного решения
Создание собственного пользовательского компонента загрузки файлов для React имеет смысл, когда вам нужно специфическое поведение или вы хотите минимизировать размер бандла. Библиотеки вроде react-dropzone обрабатывают граничные случаи, которые вы можете упустить — но они также добавляют зависимости и подходы, которые могут вам не подойти.
Основной паттерн прост: скрытый input, доступный label, опциональная зона drop, валидация и XHR для прогресса. Всё остальное — это доработка.
Заключение
Пользовательский компонент загрузки файлов даёт вам точный контроль над поведением, стилизацией и размером бандла. Ключевые ограничения просты: file inputs должны оставаться неконтролируемыми, клиентская валидация дополняет, но никогда не заменяет серверные проверки, а XMLHttpRequest остаётся надёжным выбором для отслеживания прогресса. Начните с доступных основ, добавьте drag-and-drop как опциональное улучшение и обрабатывайте очистку памяти для предпросмотра. Создавать ли собственное решение или использовать библиотеку — зависит от ваших конкретных потребностей, но теперь вы понимаете, что происходит под капотом в любом случае.
Часто задаваемые вопросы
Безопасность браузера предотвращает программное присвоение значений file inputs. Для сброса либо вызовите inputRef.current.value = '' используя ref (что работает для очистки, но не для установки), либо размонтируйте и перемонтируйте input, изменив его prop key. Оба подхода эффективно очищают выбор, соблюдая ограничения браузера.
Создайте массив состояний загрузки, по одному на файл. Каждый объект состояния отслеживает свой собственный прогресс, статус и любые ошибки. Используйте имя файла или сгенерированный ID в качестве ключа. При загрузке пройдитесь по файлам и создайте отдельные экземпляры XMLHttpRequest, обновляя соответствующую запись состояния в каждом callback прогресса.
Событие dragLeave срабатывает при перемещении между родительским и дочерними элементами. Исправьте это, проверяя, содержится ли связанная цель внутри зоны drop перед установкой isDragging в false, или используйте счётчик, который увеличивается на dragEnter и уменьшается на dragLeave, обновляя состояние только когда счётчик достигает нуля.
Используйте FormData в большинстве случаев. Он эффективно обрабатывает бинарные данные и поддерживает кодировку multipart/form-data, которую ожидают серверы. Base64-кодирование увеличивает размер файла примерно на 33% и требует больше обработки с обеих сторон. Используйте base64 только когда ваш API специально этого требует или при встраивании небольших файлов в JSON-payload.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.