Proper way to create a controlled `<input type='file'>` element in React
Asked Answered
B

1

6

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!

Brander answered 25/4, 2023 at 16:1 Comment(0)
A
11

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://mcmap.net/q/554923/-how-to-change-a-file-input-39-s-filelist-programmatically 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

Aleris answered 26/4, 2023 at 8:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.