Created
December 17, 2024 20:46
-
-
Save tlux/65875df805dd400a1df994f7f8709609 to your computer and use it in GitHub Desktop.
React hook to trigger file selection dialog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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