2

Most <input> elements use a value prop to set the value, and so they can be externally controlled by a parent.

However <input type='file'> sets its value from a files attribute, and I'm struggling to make that work correctly. files can not be set directly as a prop, but it can be set on the DOM directly via a ref, so I use useEffect() to accomplish this:

import React, { useEffect, useRef } from "react";

const FileInput = (props) => {
  const { value } = props;
  const inputRef = useRef();
  
  useEffect(() => {
    if (value === "") {
      inputRef.current.value = "";
    } else {
      inputRef.current.files = value;
    }
  }, [value]);
  
  return <input type="file" ref={inputRef} />;
};

export default FileInput;

I'd like to include an onChange() handler for when the user selects a file, but the <FileList> object is tied to the DOM and I get an error when trying to use it to set the value:

DOMException: An attempt was made to use an object that is not, or is no longer, usable

I keep going in circles on the "right" way to write a controlled form input. Is there a way to set the files attribute and value attribute correctly?

Thanks!

user2490003
  • 10,706
  • 17
  • 79
  • 155

1 Answers1

3

First of all, we know from this How to set a value to a file input in HTML? question that we can't set the value property for <input type='file'/> element programmatically.

Secondly, we know from this https://stackoverflow.com/a/68182158/6463558 answer that we can set the files property via DataTransfer.

So the FileInput component could be:

import { useEffect, useRef } from 'react';
import * as React from 'react';

export type FileInputProps = {
  fileList: File[];
  onChange(fileList: FileList): void;
};
const FileInput = ({ fileList = [], onChange }: FileInputProps) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (inputRef.current) {
      const dataTransfer = new DataTransfer();
      fileList.forEach((file) => dataTransfer.items.add(file));
      inputRef.current.files = dataTransfer.files;
    }
  }, [fileList]);

  return (
    <input
      type="file"
      ref={inputRef}
      data-testid="uploader"
      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
        onChange(e.target.files);
      }}
    />
  );
};

export default FileInput;

For a live demo, see stackblitz

Lin Du
  • 88,126
  • 95
  • 281
  • 483