Hej, chce stworzyć komponent Uploader
ktory bedzie zarowno kontrolowany jak i nie kontrolowany. Potrzebuje go w wersji kontrolowanej zeby moc go uzyć z biblioteka react-hook-form
. Tak wyglada moj kod:
export function Uploader({
multiple = true,
allowedExtensions,
instantUpload = false,
maxSize,
files: valueFromProps,
defaultFiles: defaultValue,
Trigger,
Container,
onChange: onChangeFromProps,
}: UploaderProps) {
// A component can be considered controlled when its value prop is
// not undefined.
const isControlled = typeof valueFromProps != 'undefined';
// When a component is not controlled, it can have a defaultValue.
const hasDefaultValue = typeof defaultValue != 'undefined';
// If a defaultValue is specified, we will use it as our initial
// state. Otherwise, we will simply use an empty array.
const [internalValue, setInternalValue] = useState<UploaderFile[]>(
hasDefaultValue ? defaultValue : []
);
// Internally, we need to deal with some value. Depending on whether
// the component is controlled or not, that value comes from its
// props or from its internal state.
const files = isControlled ? valueFromProps : internalValue;
const onChange = (value: UploaderFile[]) => {
// If exists, we always call onChange callback passed in props
// We do this even if there is no props.value (and the component
// is uncontrolled.)
if (onChangeFromProps) {
onChangeFromProps(value);
}
// If the component is uncontrolled, we need to update our
// internal value here.
if (!isControlled) {
setInternalValue(value);
}
};
const fileInputRef = useRef<HTMLInputElement>(null);
const mutationUploadFile = useSaveFile({});
const mutationRemoveFile = useRemoveFile();
const { toast } = useToast();
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
let inputFiles: UploaderFile[] = Array.from(event.target.files || []).map(
(file) => ({
file: file,
src: URL.createObjectURL(file),
extension: getFileExtension(file.name),
progress: 0,
uploaded: false,
})
);
if (!inputFiles.length) return;
onChange([...files, ...inputFiles]);
if (instantUpload) {
for (let file of inputFiles) {
handleUpload(file);
}
}
fileInputRef.current!.value = '';
};
const handleUpload = (file: UploaderFile) => {
mutationUploadFile.mutate(
{
file: file.file!,
onUploadProgress(progress) {
onChange(
files.map((item) => {
if (item.file === file.file)
return {
...file,
progress: Math.floor(
(progress.loaded / progress.total!) * 100
),
};
return item;
})
);
},
},
{
onSuccess(data) {
onChange(
files.map((item) => {
if (item.file === file.file)
return {
...item,
uploaded: true,
id: data.data.id,
src: data.data.url,
uploadedFile: data.data,
};
return item;
})
);
},
onError(error, variables, context) {},
}
);
};
const handleRemoveUploaderFile = (file: UploaderFile) => {
if (file.uploaded) {
mutationRemoveFile.mutate(file.id!, {
onSuccess: () => {
onChange(files.filter((item) => item.file !== file.file));
},
});
} else {
onChange(files.filter((item) => item.file?.name !== file.file?.name));
}
};
return (
<UploaderContext.Provider
value={{
files,
onChange,
handleUpload,
handleRemoveUploaderFile,
fileInputRef,
}}
>
<div className='w-full'>
<input
type='file'
name='file'
hidden
multiple={multiple}
ref={fileInputRef}
onChange={handleChange}
/>
{Trigger ? <Trigger /> : <DefaultUploadTrigger />}
{Container ? <Container /> : <DefaultUploadContainer />}
</div>
</UploaderContext.Provider>
);
}
A tak wyglada moj Uploader
dla formularzy:
export function FormUploader<T extends FieldValues>({
control,
name,
label,
errors,
...props
}: FormUploaderProps<T>) {
return (
<div className='grid gap-2'>
{label && <Label htmlFor={name}>{label}</Label>}
<Controller
name={name}
control={control}
render={({ field: { onChange, value } }) => {
return (
<div className='flex flex-col'>
<Uploader
{...props}
files={!props.multiple ? (value ? [value] : []) : value}
onChange={(files) =>
onChange(
props.multiple
? files
: files.length > 0
? files?.[0]
: undefined
)
}
/>
{errors[name] ? (
<p className='text-xs text-red-600'>
{errors[name]?.message?.toString()}
</p>
) : null}
</div>
);
}}
/>
</div>
);
}
Problem sie pojawia w momencie uploadowania pliku, a dokladnie w ponizszym fragmencie gdzie files
jest pusta macierza mimo ze w samym FormUploader
ze value
nie jest puste a wybranym wczesniej plikiem:
mutationUploadFile.mutate(
{
file: file.file!,
onUploadProgress(progress) {
onChange(
files.map((item) => {
if (item.file === file.file)
return {
...file,
progress: Math.floor(
(progress.loaded / progress.total!) * 100
),
};
return item;
})
);
},
},
{
onSuccess(data) {
onChange(
files.map((item) => {
if (item.file === file.file)
return {
...item,
uploaded: true,
id: data.data.id,
src: data.data.url,
uploadedFile: data.data,
};
return item;
})
);
},
onError(error, variables, context) {},
}
);
W wersji niekontrolowanej wszystko dziala jak nalezy, czyli progress
jest updatowany i files
nie staja sie pusta macierza. Wydaje mi sie ze z jakiegos powodu Uploader
zapamietuje poprzednia wartosc files
zamiast tej nowo podanej jako props. Jak rozwiazac ten problem?
EDIT
Uzylem ref
a zeby trzymac files
i teraz dziala:
// Internally, we need to deal with some value. Depending on whether
// the component is controlled or not, that value comes from its
// props or from its internal state.
const files = useRef<UploaderFile[]>([]);
if (isControlled) {
files.current = valueFromProps;
} else files.current = internalValue;
Pytanie dlaczego? I czy nie ma lepszego sposobu na to? Probowalem zrobic osobny state
dla files
ale wciaz files
bylo puste przy uploadowaniu.