0

Hi am trying to create a simple multi file component, But the files state is not behaving as expected.

The FileUpload component works fine and when the user chooses a file and it starts uploading the onStart prop method is called. And then when it finishes successfully the 'onFinish' prop method is called. This code seems fine to the point of consoling the object in the onfinish method. it consoles an old value of the object before it was modified by the onStart Method. I expected the file object value in the console to include the buffer key since it was added when the onStart method was called but it's not there.

Example initial state files should be [] when the use effect is called on the state files should be updated to [{_id:"example_unique_id"}] then a button for upload will appear and when user chooses a file and onStart modifies the object and the state should be updated to [{_id:"example_unique_id", buffer:{}] and finally when it finishes files should be [{_id:"example_unique_id", buffer:{}] but instead here it returns [{_id:"example_unique_id"}].

What could I be missing out on?

Also, I have React Dev tools installed and it seems the state is updated well in the dev tools.

import React, { useState } from 'react'
import { useEffect } from 'react';
import unique_id from 'uniqid'
import FileUpload from "./../../components/FileUpload";

const InlineFileUpload = ({ onFilesChange }) => {
  const [files, setFiles] = useState([]);

  function onFinish(file, id) {
    const old_object = files.filter((file) => file._id == id)[0];
    console.log("old object on after upload", old_object);
  }

  const addFile = (file, id) => {
    const old_object = files.filter((file) => file._id == id)[0];
    const index = files.indexOf(old_object);
    const new_files = [...files];
    new_files.splice(index, 1, { ...old_object, buffer: file });
    setFiles(new_files);
  };

  useEffect(() => {
    const new_attachments = files.filter(({ buffer }) => buffer == undefined);
    if (new_attachments.length == 0) {
      setFiles([...files, { _id: unique_id() }]);
    }

    const links = files.filter((file) => file.file !== undefined);

    if (links.length !== 0) {
      onFilesChange(links);
    }
  }, [files]);

  return (
    <>
      {files.map((file) => {
          const { _id } = file;
        return ( <FileUpload
              key={_id}
              id={_id}
              onStart={(e) => addFile(e, _id)}
              onFinish={(e) => onFinish(e, _id)}
            />
        );
      })}
    </>
  );
};

export default InlineFileUpload

iamafasha
  • 848
  • 10
  • 29
  • My guess is that `onFinish` is called *before* the state update is processed, thus logging the state from the current render cycle and not the state of the *next* render cycle after state has been updated. Can you share `FileUpload` code implementation? – Drew Reese Jul 03 '21 at 06:16

2 Answers2

0

I think the problem is caused by the fact that your this code is not updating the state:

  const addFile = (file, id) => {
  const old_object = files.filter((file) => file._id == id)[0];
    const index = files.indexOf(old_object);
    const new_files = [...files];
    new_files.splice(index, 1, { ...old_object, buffer: file });
    setFiles(new_files);
}

files looks like an array of objects. Spread operator will not do a deep copy of this array. There are a lot of examples on the internet, here is one.

let newArr = [{a : 1, b : 2},
             {x : 1, y : 2},
              {p: 1, q: 2}];

let arr = [...newArr];
arr[0]['a'] = 22;
console.log(arr);
console.log(newArr);

So your new_files is the same array. Splice must be making some modifications but that is in place. So when you are doing this setFiles(new_files);, you are basically setting the same reference of object as your newState. React will not detect a change, and nothing gets updated.

You have the option to implement a deep copy method for your specific code or use lodash cloneDeep. Looking at your code, this might work for you : const new_files = JSON.parse(JSON.stringify(files)). It is a little slow, and you might lose out on properties which have values such as functions or symbols. Read

Tushar Shahi
  • 16,452
  • 1
  • 18
  • 39
  • 1
    No, `newFiles` is a *new* array reference, but you are correct that it's a shallow copy, all the elements are references to the objects in the `files` array. The splice is *replacing* an object at the `index` with a *new* object reference with the old element spread into it with a new `buffer` property. This looks to be an appropriate state update to me. – Drew Reese Jul 03 '21 at 06:13
  • With the help of the answer and the comment, I was able to fix my issue, thank you, But I also end up using setFiles(current => newState) way instead of just setFiles(). Instead of using lodash – iamafasha Jul 03 '21 at 08:09
0

The reason you are getting the old log is because of closures.

When you do setFiles(new_files) inside addFiles function. React updates the state asynchronously, but the new state is available on next render.

The onFinish function that will be called is still from the first render, referencing files of the that render. The new render has the reference to the updated files, so next time when you log again, you will be getting the correct value.

If it's just about logging, wrap it in a useEffect hook,

useEffect(() => {
  console.log(files)
}, [files);

If it's about using it in the onFinish handler, there are answers which explore these option.

Badal Saibo
  • 2,499
  • 11
  • 23