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 FormUploaderze 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 refa 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.