Skip to content

Instantly share code, notes, and snippets.

@tlux
Created December 17, 2024 20:46
Show Gist options
  • Save tlux/65875df805dd400a1df994f7f8709609 to your computer and use it in GitHub Desktop.
Save tlux/65875df805dd400a1df994f7f8709609 to your computer and use it in GitHub Desktop.
React hook to trigger file selection dialog
import { useCallback, useEffect, useRef, useState } from 'react';
function wrapArray<T>(value: T | T[]): T[] {
return Array.isArray(value) ? value : [value];
}
type UseFileOnSelectCallbackArg<TMultiple extends boolean | undefined> =
TMultiple extends true ? FileList : File;
type UseFileOnSelectCallback<TMultiple extends boolean | undefined = false> = (
files: UseFileOnSelectCallbackArg<TMultiple> | null,
) => void;
interface UseFileOptions<TMultiple extends boolean | undefined> {
/**
* Content types to filter the selectable files by.
*/
accept?: string | string[];
/**
* Indicates whether multiple files can be selected.
*/
multiple?: TMultiple;
/**
* Callback that is invoked when a file is selected.
*
* @param files A file list or null.
*/
onSelect?: UseFileOnSelectCallback<TMultiple>;
}
export type UseFile<TMultiple extends boolean | undefined = false> =
TMultiple extends true
? { files: FileList | null; reset: () => void; select: () => void }
: { file: File | null; reset: () => void; select: () => void };
export function useFile<TMultiple extends boolean | undefined = false>({
accept,
multiple = false,
onSelect,
}: UseFileOptions<TMultiple> = {}): UseFile<TMultiple> {
const [files, setFiles] = useState<FileList | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', wrapArray(accept).join(','));
if (multiple) input.setAttribute('multiple', '');
input.style.display = 'none';
input.onchange = (event) => {
const target = event.target as HTMLInputElement;
const fileList = target.files;
if (!fileList) return;
setFiles(fileList);
if (multiple) {
onSelect?.(fileList as UseFileOnSelectCallbackArg<TMultiple>);
return;
}
const file = fileList.item(0);
if (!file) return;
onSelect?.(file as UseFileOnSelectCallbackArg<TMultiple>);
};
document.body.appendChild(input);
inputRef.current = input;
return () => {
input.remove();
inputRef.current = null;
};
}, [accept, multiple, onSelect]);
const select = useCallback(() => {
inputRef.current?.click();
}, []);
const reset = useCallback(() => {
setFiles(null);
onSelect?.(null);
}, [onSelect]);
if (multiple) {
return { files, reset, select } as UseFile<TMultiple>;
}
const file = files?.item(0) ?? null;
return { file, reset, select } as UseFile<TMultiple>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment