useFileReader - хук для чтения файлов
Категория: Browser / Async
Мотивация
Стандартные способы прочитать File/Blob в браузере — FileReader.readAsText/readAsArrayBuffer или промис-методы blob.text()/blob.arrayBuffer() — читают файл целиком за один раз. Для больших файлов (видео, дампы БД, датасеты, логи) это создаёт несколько проблем:
- весь файл разом попадает в память как одна строка/буфер, что может уронить вкладку на файлах в сотни МБ-ГБ;
- нет прогресса по мере чтения — только событие
onload в самом конце;
- невозможно обработать файл потоково (посчитать инкрементальный хэш, распарсить CSV/NDJSON построчно, провалидировать содержимое) без предварительной полной загрузки в память;
- нет встроенной паузы/резюма/отмены на середине чтения;
- нет ретраев отдельного фрагмента при сбое обработки.
File/Blob поддерживают .slice(start, end), что позволяет читать/обрабатывать файл кусками (chunk'ами) произвольного размера, но каждый раз писать этот цикл руками (slice → decode → await callback → следующий slice, плюс пауза/отмена/ретраи) — рутина, которую хочется закрыть один раз хуком.
Дополнительный мотив: такой хук может стать общим фундаментом для более специфичных сценариев — например, для загрузки больших файлов чанками на бэкенд (см. предложение useChunkedFileUpload). Чтобы это работало, useFileReader должен уметь отдавать в колбэк сырой Blob-чанк (без обязательной декодизации в текст/ArrayBuffer) и поддерживать параллельную обработку чанков (concurrency) и ретраи — тогда useChunkedFileUpload можно будет реализовать как тонкую обёртку над этим хуком, а не как отдельную реализацию с дублированной логикой нарезки/очереди/паузы.
Предлагаемое API
export type FileReaderDataType = 'ArrayBuffer' | 'Text' | 'DataURL';
export type FileReaderChunk<DataType extends FileReaderDataType | undefined> = DataType extends 'ArrayBuffer'
? ArrayBuffer
: DataType extends 'Text' | 'DataURL'
? string
: Blob;
export interface UseFileReaderOptions<DataType extends FileReaderDataType | undefined = undefined> {
/** Размер одного чанка в байтах */
chunkSize?: number;
/** Количество чанков, обрабатываемых параллельно (1 = последовательно) */
concurrency?: number;
/** Количество повторных попыток на чанк перед тем, как считать его ошибкой */
retries?: number;
/** Как декодировать сырой Blob-чанк перед передачей в onChunk. Без указания в колбэк приходит сырой Blob (удобно, если чанк надо просто переслать дальше, например в сеть) */
dataType?: DataType;
/** Обрабатывает один чанк. Может быть async - следующий чанк (или следующая партия при concurrency > 1) стартует по мере освобождения слотов */
onChunk: (
chunk: FileReaderChunk<DataType>,
meta: { index: number; offset: number; size: number; totalChunks: number; totalBytes: number; signal: AbortSignal }
) => void | Promise<void>;
/** Вызывается после каждого успешно обработанного чанка (для прогресса/сохранения состояния) */
onChunkComplete?: (index: number, totalChunks: number) => void;
/** Вызывается один раз, когда все чанки успешно обработаны */
onComplete?: () => void;
/** Вызывается, если чанк не обработался после исчерпания ретраев */
onError?: (error: unknown, index: number) => void;
}
export interface UseFileReaderReturn {
/** 'idle' | 'reading' | 'paused' | 'error' | 'done' | 'aborted' */
status: 'idle' | 'reading' | 'paused' | 'error' | 'done' | 'aborted';
/** Общий прогресс, 0-100 */
progress: number;
/** Сколько байт уже обработано */
bytesRead: number;
/** Полный размер файла в байтах */
totalBytes: number;
/** Запускает чтение/обработку */
start: () => void;
/** Приостанавливает после текущего(их) в-полёте чанка(ов) */
pause: () => void;
/** Продолжает с места остановки */
resume: () => void;
/** Прерывает чтение и сбрасывает прогресс */
abort: () => void;
/** Повторяет конкретный упавший чанк (или все упавшие, если index не передан) */
retry: (index?: number) => void;
}
export interface UseFileReader {
(file: File | Blob, options: UseFileReaderOptions<undefined>): UseFileReaderReturn;
(file: File | Blob, options: UseFileReaderOptions<'ArrayBuffer'>): UseFileReaderReturn;
(file: File | Blob, options: UseFileReaderOptions<'Text' | 'DataURL'>): UseFileReaderReturn;
}
export const useFileReader = ((file: File | Blob, options: UseFileReaderOptions<any>): UseFileReaderReturn => {
/* ... */
}) as UseFileReader;
Пример использования
// Инкрементальный подсчёт SHA-256 большого файла без загрузки его целиком в память
function FileHashDemo() {
const [file, setFile] = useState<File | null>(null);
const [hash, setHash] = useState<string | null>(null);
const { open } = useFileDialog((files) => setFile(files?.[0] ?? null), { multiple: false });
const chunksRef = useRef<ArrayBuffer[]>([]);
const { status, progress, start, pause, resume, abort } = useFileReader(file!, {
chunkSize: 8 * 1024 * 1024, // 8MB
dataType: 'ArrayBuffer',
onChunk: (chunk) => {
chunksRef.current.push(chunk);
},
onComplete: async () => {
const full = await new Blob(chunksRef.current).arrayBuffer();
const digest = await crypto.subtle.digest('SHA-256', full);
setHash([...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join(''));
}
});
return (
<div>
<button onClick={() => open()}>Выбрать файл</button>
{file && (
<div>
<button onClick={start} disabled={status === 'reading'}>Старт</button>
<button onClick={pause} disabled={status !== 'reading'}>Пауза</button>
<button onClick={resume} disabled={status !== 'paused'}>Продолжить</button>
<button onClick={abort}>Отмена</button>
<progress value={progress} max={100} />
<span>{status}</span>
{hash && <div>SHA-256: {hash}</div>}
</div>
)}
</div>
);
}
useFileReader- хук для чтения файловКатегория: Browser / Async
Мотивация
Стандартные способы прочитать
File/Blobв браузере —FileReader.readAsText/readAsArrayBufferили промис-методыblob.text()/blob.arrayBuffer()— читают файл целиком за один раз. Для больших файлов (видео, дампы БД, датасеты, логи) это создаёт несколько проблем:onloadв самом конце;File/Blobподдерживают.slice(start, end), что позволяет читать/обрабатывать файл кусками (chunk'ами) произвольного размера, но каждый раз писать этот цикл руками (slice → decode → await callback → следующий slice, плюс пауза/отмена/ретраи) — рутина, которую хочется закрыть один раз хуком.Дополнительный мотив: такой хук может стать общим фундаментом для более специфичных сценариев — например, для загрузки больших файлов чанками на бэкенд (см. предложение
useChunkedFileUpload). Чтобы это работало,useFileReaderдолжен уметь отдавать в колбэк сыройBlob-чанк (без обязательной декодизации в текст/ArrayBuffer) и поддерживать параллельную обработку чанков (concurrency) и ретраи — тогдаuseChunkedFileUploadможно будет реализовать как тонкую обёртку над этим хуком, а не как отдельную реализацию с дублированной логикой нарезки/очереди/паузы.Предлагаемое API
Пример использования