Найти в Дзене
IlyaDev

Индикация загрузки документа. React.

Привет друг! В этой статье попробуем реализовать компонент - индикатор загрузки для отправленного документа с клиента. Чего мы хотим добиться? тут 🥸. И так - поехали !🛼 Накидаем сходу немного стилей. И спрячем input type="file", отрисовав вместо него кнопку - 'Выбрать файл'. Подготовим состояние для сохранения файла. import {ChangeEvent, FC, useRef, useState} from 'react';
export const FileUpload = () => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setSelectedFile(event.target.files[0]);
}
};
const handleUpload = async () => {
if (!selectedFile) {
alert('Please select a file to upload!');
return;
}
};
const handleButtonClick = () => { // Триггерим клик по скрытому input inputRef.current?.click();
};
return (
<div sty
Оглавление

Привет друг! В этой статье попробуем реализовать компонент - индикатор загрузки для отправленного документа с клиента.

Чего мы хотим добиться? тут 🥸.

И так - поехали !🛼

1. Создаём компоненту FileUpload.tsx.

Накидаем сходу немного стилей. И спрячем input type="file", отрисовав вместо него кнопку - 'Выбрать файл'. Подготовим состояние для сохранения файла.

import {ChangeEvent, FC, useRef, useState} from 'react';

export const FileUpload = () => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);

const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setSelectedFile(event.target.files[0]);
}
};

const handleUpload = async () => {
if (!selectedFile) {
alert('Please select a file to upload!');
return;
}
};

const handleButtonClick = () => {
// Триггерим клик по скрытому input
inputRef.current?.click();
};

return (
<div style={{width: '300px', margin: '0 auto', textAlign: 'center', height: '100%'}}>
<div style={{display: 'flex', flexDirection: 'column', gap: '10px'}}>
<input
type="file"
ref={inputRef}
onChange={handleFileChange}
style={{
display:'none',
}}
/>
<button
type="button"
onClick={handleButtonClick}
style={{
padding: '10px 20px',
backgroundColor: '#4CAF50',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
fontSize: '16px',
transition: 'background-color 0.3s ease',
}}
>
Выбрать файл
</button>
{selectedFile && <p style={{fontSize: '14px', color: '#333'}}>Выбран файл: {selectedFile.name}</p>}
<button
onClick={handleUpload}
disabled={!selectedFile}
style={{
padding: '10px 20px',
backgroundColor: `${!selectedFile ? '#898f89' : '#4CAF50'}`,
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: `${!selectedFile ? 'no-drop' : 'pointer'}`,
fontSize: '16px',
transition: 'background-color 0.3s ease',
}}
>
Загрузить
</button>
</div>
</div>
);
};

2. Дополняем состояния "полосы загрузки", флажок загружен или нет. А так же пишем фейковый запрос.

  • Доп состояния:
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
const [isUploading, setIsUploading] = useState<boolean>(false);

  • Пишем фейковый запрос:
const fakeUploadRequest = async (file: File) => {
const fileSizeInMB = file.size / (1024 * 1024); // Размер файла в мегабайтах
const uploadSpeed = 1; // Условная скорость "загрузки" (МБ/с)
const totalUploadTime = fileSizeInMB / uploadSpeed; // Общее время загрузки в секундах

const interval = 100; // Интервал обновления прогресса (мс)
const totalSteps =
Math.ceil((totalUploadTime * 1000) / interval);
let currentStep = 0;

return new Promise<void>((resolve) => {
const intervalId = setInterval(() => {
currentStep++;
const progress =
Math.min((currentStep / totalSteps) * 100, 100);
setUploadProgress(progress);

if (progress === 100) {
clearInterval(intervalId);
resolve();
}
}, interval);
});
};

  • Дописываем существующие функции handleFileChange и handleUpload:
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setSelectedFile(event.target.files[0]);
setUploadProgress(null); // Сбрасываем прогресс при выборе нового файла
}
};
const handleUpload = async () => {
if (!selectedFile) {
alert('Please select a file to upload!');
return;
}

setIsUploading(true);
try {
await fakeUploadRequest(selectedFile);
alert('File uploaded successfully!');
} catch (error) {
console.error('Error during upload simulation:', error);
alert('Failed to upload file!');
} finally {
setIsUploading(false);
setSelectedFile(null);
}
};

3. Как может выглядеть реальный запрос:

Можем использовать отца всех современных инструментов для взаимодействия с серверной частью апки - XMLHttpRequest():

const uploadFileToServer = async (
file: File,
onProgress: (progress: number) => void,
) => {
return new Promise<void>((resolve, reject) => {
const formData = new FormData();
formData.append('file', file);

const xhr = new XMLHttpRequest();

xhr.open('POST', 'https://your-server-url.com/upload', true);

// Отслеживание прогресса загрузки
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
onProgress(progress); // Обновляем прогресс через callback
}
};

xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
reject(new Error(`Upload failed: ${xhr.statusText}`));
}
};

xhr.onerror = () => reject(new Error('Network error'));
xhr.send(formData);
});
};
const handleUploadRequest = async (file: File) => {
await uploadFileToServer(file, setUploadProgress);
};

Ну или axios:

-2
import axios from 'axios';

const uploadFileWithAxios = async (
file: File,
onProgress: (progress: number) => void
) => {
const formData = new FormData();
formData.append('file', file);

await axios.post('https://your-server-url.com/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (event) => {
if (event.total) {
const progress =
Math.round((event.loaded / event.total) * 100);
onProgress(progress); // Обновляем прогресс
}
},
});
};

Ну или еще чего нибудь😎 Если совсем отчаянный можно использовать fetch. Но fetch не предоставляет прямой возможности отслеживания прогресса загрузки файла. Однако можно комбинировать fetch с низкоуровневым API ReadableStream для реализации аналогичного функционала. Ну и в таком случаем каждый чанк будет отправлять запрос, да и сервер придётся подготовить для подобных танцев с бубнами 🙂 Короч - разберёшься.

4. Дописываем оставшуюся разметку для полосочки загрузки и для цифровой индикации той самой загрузки:

{uploadProgress !== null && (
<div style={{marginTop: '20px'}}>
<div
style={{
width: '100%',
height: '20px',
backgroundColor: '#e0e0e0',
borderRadius: '5px',
overflow: 'hidden',
}}
>
<div
style={{
width: `${uploadProgress}%`,
height: '100%',
backgroundColor: '#76c7c0',
transition: 'width 0.1s ease',
}}
/>
</div>
<p style={{color: 'black'}}>{uploadProgress.toFixed(2)}%</p>
</div>
)}

5. Собираем всё вместе. Итоговый код:

import {ChangeEvent, useRef, useState} from 'react';

type Nullable<T> = null | T;

export const FileUpload = () => {
const [selectedFile, setSelectedFile] = useState<Nullable<File>>(null);
const [uploadProgress, setUploadProgress] = useState<Nullable<number>>(null);
const [isUploading, setIsUploading] = useState<boolean>(false);

const inputRef = useRef<Nullable<HTMLInputElement>>(null);

const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setSelectedFile(event.target.files[0]);
setUploadProgress(null);
}
};

const fakeUploadRequest = async (file: File) => {
const fileSizeInMB = file.size / (1024 * 1024);
const uploadSpeed = 1;
const totalUploadTime = fileSizeInMB / uploadSpeed;

const interval = 100;
const totalSteps =
Math.ceil((totalUploadTime * 1000) / interval);
let currentStep = 0;

return new Promise<void>((resolve) => {
const intervalId = setInterval(() => {
currentStep++;
const progress =
Math.min((currentStep / totalSteps) * 100, 100);
setUploadProgress(progress);

if (progress === 100) {
clearInterval(intervalId);
resolve();
}
}, interval);
});
};

const handleUpload = async () => {
if (!selectedFile) {
alert('Please select a file to upload!');
return;
}

setIsUploading(true);
try {
await fakeUploadRequest(selectedFile);
alert('File uploaded successfully!');
} catch (error) {
console.error('Error during upload simulation:', error);
alert('Failed to upload file!');
} finally {
setIsUploading(false);
setSelectedFile(null);
}
};

const handleButtonClick = () => {
inputRef.current?.click();
};

return (
<div style={{width: '300px', margin: '0 auto', textAlign: 'center', height: '100%'}}>
<div style={{display: 'flex', flexDirection: 'column', gap: '10px'}}>
<input
type="file"
ref={inputRef}
onChange={handleFileChange}
style={{
display:'none',
}}
/>
<button
type="button"
onClick={handleButtonClick}
style={{
padding: '10px 20px',
backgroundColor: '#4CAF50',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
fontSize: '16px',
transition: 'background-color 0.3s ease',
}}
onMouseOver={(e) => (e.currentTarget.style.backgroundColor = '#45a049')}
onMouseOut={(e) => (e.currentTarget.style.backgroundColor = '#4CAF50')}
>
Выбрать файл
</button>
{selectedFile && <p style={{fontSize: '14px', color: '#333'}}>Выбран файл: {selectedFile.name}</p>}
<button
onClick={handleUpload}
disabled={isUploading || !selectedFile}
style={{
padding: '10px 20px',
backgroundColor: `${isUploading || !selectedFile ? '#898f89' : '#4CAF50'}`,
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: `${isUploading || !selectedFile ? 'no-drop' : 'pointer'}`,
fontSize: '16px',
transition: 'background-color 0.3s ease',
}}
>
{isUploading ? 'Загрузка...' : 'Загружен'}
</button>
</div>

{uploadProgress !== null && (
<div style={{marginTop: '20px'}}>
<div
style={{
width: '100%',
height: '20px',
backgroundColor: '#e0e0e0',
borderRadius: '5px',
overflow: 'hidden',
}}
>
<div
style={{
width: `${uploadProgress}%`,
height: '100%',
backgroundColor: '#76c7c0',
transition: 'width 0.1s ease',
}}
/>
</div>
<p style={{color: 'black'}}>{uploadProgress.toFixed(2)}%</p>
</div>
)}
</div>
);
};

Понятное дело, логику лучше вынести из компоненты, ну и в принципе для вариативности (использовании в нескольких местах - ну мало ли ≽^•༚• ྀི≼ ) пропсами закидывать асинк функшн, и uploadProgress со всеми вытекающими! Да и вообще мы молодцы 🧸!

Спасибо за внимание! До Новых Встреч!🤗🤗🤗

-3