-3

I'm trying to push an object {link:href} to the state in my project. When I try to add href in {link:href} it says it is undefined:

image

Here is my code:

class Download extends React.Component {
  constructor(props) {
    super(props);

    this.handleClick = this.handleClick.bind(this);

    this.inputRef = React.createRef();

    this.state = {
      files: []
    };
  }

  handleClick = () => {
    const node = this.inputRef.current;

    const self = this;

    let file;
    let name;
    let href;

    node.addEventListener("change", function() {
      const fileList = [];

      for (let x = 0, xlen = this.files.length; x < xlen; x++) {
        file = this.files[x];
        name = file.name;

        fileList.push({ name: name });
        let reader = new FileReader();
        reader.onload = e => {
          href = e.target.result;
        };

        fileList.push({ link: href });
        reader.readAsDataURL(file);
      }

      self.setState({ files: fileList });
      console.log(self.state);
    });
  };

  render() {
    return (
      <div className="input">
        <input
          onClick={this.handleClick}
          id="upload-file"
          className="inputName"
          type="file"
          multiple
          ref={this.inputRef}
        />
        <div>
          <ul ref={this.ulRef}>
            {this.state.files.map((file, index) => (
              <li key={index}>
                <Link to={file.link}>{file.name}</Link>
              </li>
            ))}
          </ul>
        </div>
      </div>
    );
  }
}

export default Download;

The reason I'm trying to push {link:href} after the reader.onload function is because the reader.onload function gets loaded after the for loop runs x number of times and it only displays the last item.

DDavis25
  • 1,149
  • 1
  • 13
  • 25
  • By writing `reader.onload = e => { href = e.target.result; };`, you don't actually call that function, you're just editing it. –  Dec 13 '19 at 19:55
  • 1
    You don't need `ref` to get the files, React has a `onChange` event prop for input elements. – Emile Bergeron Dec 13 '19 at 19:56
  • 1
    `.push` mutates the current state in place, see [how to update an array in the state](https://stackoverflow.com/q/26253351/1218980). – Emile Bergeron Dec 13 '19 at 19:57
  • 1
    And here's [why you should not mutate the state](https://stackoverflow.com/q/37755997/1218980). – Emile Bergeron Dec 13 '19 at 19:57
  • Also, it's useless to log the state just after setting it since the [`setState` function is async](https://stackoverflow.com/q/30782948/1218980). – Emile Bergeron Dec 13 '19 at 19:59
  • Anywhere you're using `const self = this;` could be replaced with an arrow function. – Emile Bergeron Dec 13 '19 at 20:00
  • 1
    And since you're already using an arrow function, `this.handleClick = this.handleClick.bind(this);` is useless in the constructor. – Emile Bergeron Dec 13 '19 at 20:00
  • 1
    @EmileBergeron to resume, OP should just rebuild the entire component. –  Dec 13 '19 at 20:01
  • Also change `for (let x = 0, xlen = this.files.length; x < xlen; x++)` to `for (const file of this.files)` since he's not using `x`. –  Dec 13 '19 at 20:03
  • @EmileBergeron but I'm not adding to the state directly. I'm pushing to a separate array and adding that array to the state. – DDavis25 Dec 13 '19 at 20:06
  • @DDavis25 Misread the code, my bad, you're right! – Emile Bergeron Dec 13 '19 at 20:08
  • And the reason it is `undefined` is because `reader.onload` is an async function, so you're pushing to the array before `href = e.target.result;` actually runs. – Emile Bergeron Dec 13 '19 at 20:09
  • @EmileBergeron I tried using an IIFE for the reader.onload function but it didn't change anything. What do you think I should do? – DDavis25 Dec 13 '19 at 20:13
  • There are a couple of questions that already tackle file inputs with React. Like https://stackoverflow.com/q/55831213/1218980 or https://stackoverflow.com/q/54321502/1218980 – Emile Bergeron Dec 13 '19 at 20:18

1 Answers1

0

First thing, you don't need to use any refs for this. And you don't need to add an event listener to node (you don't need node at all) as you can use react's onChange instead of onClick. You also don't need to use a Link for this. You can just use an anchor tag with an href which you can assign the file data to. I prefer to use a function that handles downloads as I feel it gives me more control and doesn't affect your browser's windows or DOM elements. You essentially create an anchor tag, trigger a download and then remove it.

You are also pushing the elements in incorrectly. By pushing {name: filename} and then pushing in {link: href} you have just pushed two objects into your array and both of those will be mapped over. Instead, create an object, and add both of these key value pairs to that object and push that single object in. You don't need to push anything into the array until you reach the reader.onload. By pushing something in before the reader.onload and then again within the reader.onload you're just pushing something in twice.

Add the name to the object. Remove the first push and then in reader.onload add the link value to the same object and then push the object into the array. You also should not set the state outside of the reader.onload as this will trigger a re-render and render your incomplete object resulting in error. Only set the state once within the reader.onload so that when the re-render is triggered it will have the complete object in the list.

If you want your files to persist (meaning allowing multiple files to added to your file list) then rather than setting fileList to an empty array every time the onChange is triggered set it equal to this.state.files.slice(). We can use slice as to create a new array and not mutate this.state.files directly.

Here is a working code sandbox.

As you will see I also removed your constructor as for this use case just using state = {...} is sufficient. If you prefer to use the constructor you can add it back if you like, I just took it upon myself to save you a few lines of code.

class Download extends React.Component {

  state = {
    files: []
  };

  handleClick = event => {
    let file, name, href;

    var breh = event.target;

    const fileList = this.state.files.slice();

    for (let x = 0, xlen = breh.files.length; x < xlen; x++) {
      file = breh.files[x];
      name = file.name;

      var obj = {};
      obj["name"] = name;

      let reader = new FileReader();
      reader.onload = e => {
        href = e.target.result;
        obj["link"] = href;
        fileList.push(obj);
        this.setState({ files: fileList });
      };

      reader.readAsDataURL(file);
    }
  };

  download = (uri, name) => {
    var link = document.createElement("a");
    link.download = name;
    link.href = uri;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  };

  render() {
    console.log(this.state);
    return (
      <div className="input">
        <input
          onChange={this.handleClick}
          id="upload-file"
          className="inputName"
          type="file"
          multiple
        />
        <div>
          <ul>
            {this.state.files.map((file, index) => {
              return (
                <li
                  key={index}
                  onClick={() => {
                    this.download(file.link, file.name);
                  }}
                >
                  {file.name}
                </li>
              );
            })}
          </ul>
        </div>
        <output id="list" />
      </div>
    );
  }
}

export default Download;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Matt Croak
  • 2,788
  • 2
  • 17
  • 35