Search code examples
javascriptreactjsreact-hooksuse-effectsetstate

Retaining the state after Expand and collapse | Causing React state update error


I have developed a upload functionality, upload component inside accordion. Now I want to retain the data/files that were uploaded in the system when the accordion is collapsed and expanded.

Now I am able to retain the files, but deleting them is throwing the error.

Error:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

Design

  1. Parent component/ (Class component) [parentState]
  2. Child component/Functional (upload) [maintains local state], sends data that is required by Parent component (Working)
  3. To retain the state, after collapse and expand of accordion, Moved all the [local state] data to parent state through call back function.
  4. On component did mount(functional component), took all the data and updated the loca state
  5. Deleting the files (After opening and closing the accordion), getting the above error.

CODE

/** set the state locally from parent state 
 * component did mount
 */
useEffect(() => {
 
  
  if (parentState[`${field}acceptedFiles`]) {
    setAcceptedFiles([...parentState[`${field}acceptedFiles`]]);
  }
  if (parentState[`${field}rejectedFiles`]) {
    console.log('REJECTED FILES', parentState[`${field}rejectedFiles`]);
    setRejectedFiles([...parentState[`${field}rejectedFiles`]]);
  }

}, []);


/** maintain the state in parent state
 * this is used retain the state from parent state
 */
useEffect(() => {
  const afile = {
    [`${field}acceptedFiles`]: acceptedFiles
  };
  onChange(afile);
}, [acceptedFiles]);

useEffect(() => {
  const rfile = {
    [`${field}rejectedFiles`]: rejectedFiles
  };
  onChange(rfile);
}, [rejectedFiles]);



/** deleting the rejected files, removing them from UI */
const deleteRejectedFile = (uuid) => {
  setRejectedFiles((updatedRejectedFiles) => {
    return updatedRejectedFiles.filter(rejectedFile => rejectedFile.uuid !== uuid);
  });
}

/** deleting the accepted files, removing them from UI and removing from attachments
 * Send back uploadAttachments to parent state */
const deleteAcceptedFile = (uuid) => {
  setAcceptedFiles((updatedAcceptedFiles) => {
    return updatedAcceptedFiles.filter(acceptedFile => acceptedFile.uuid !== uuid);
  });
}

const handleRejectedFiles = (rejected, docTypeError) => {
  const files = rejected.map(file => {
    const uuid = getUUID();
    return {
    name: file.name,
    uuid,
    rightIconItems: {
      onIconClick: () => deleteRejectedFile(uuid)
    },
  }});
  setRejectedFiles([...rejectedFiles, ...files]);
}

const handleAcceptedFiles = (accepted) => {
  const files = accepted.map((file) => {
    const uuid = getUUID();
    const { PENDING, INFO } = constants;
    return {
      ...file,
      file: file,
      uuid,
      rightIconItems: {
        onIconClick: () => deleteAcceptedFile(uuid)
      }
    }
  })
  setAcceptedFiles([...acceptedFiles, ...files]);
}

const onFileSelected = (accepted, rejected) => {
  handleAcceptedFiles(accepted);
  handleRejectedFiles(rejected);
};

return (
  <DragAndDropContainer>
    <FileSelector 
      onFileSelected={onFileSelected}
      acceptedFiles={acceptedFiles}
      rejectedFiles={rejectedFiles}
    />
  </DragAndDropContainer>
);

Design dataflow

Updated the Error

The error is in the delete function, as accordion is closed the component is unmounted, but setState is still pointing to its old reference [scope].

Is there a way to point the setstate to newly created component, instead of pointing to unmounted component without moving the state to parent function.

/** deleting the rejected files, removing them from UI */
const deleteRejectedFile = (uuid) => {
  setRejectedFiles((updatedRejectedFiles) => {
    return updatedRejectedFiles.filter(rejectedFile => rejectedFile.uuid !== uuid);
  });
}

Solution

  • This error was caused due to referencing of the delete method. deleteRejectedFile and deleteAcceptedFile function's setstate methods were pointing to the component which was unmounted instead of pointing to the mounted component.

    So I have created a new reference of the icon

        useEffect(() => {
          if (parentState[`${field}acceptedFiles`]) {
            const newAcceptedReferences = parentState[`${field}acceptedFiles`].map((accepted) => {
              const rightIconItems = {
                onIconClick: () => deleteAcceptedFile(accepted.uuid)
              };
              return {
                ...accepted,
                rightIconItems
              }
            });
            setAcceptedFiles(newAcceptedReferences);
          }
        
          if (parentState[`${field}rejectedFiles`]) {
            const newRejectedReferences = parentState[`${field}rejectedFiles`].map((rejected) => {
              const rightIconItems = {
                onIconClick: () => deleteRejectedFile(rejected.uuid)
              };
              return {
                ...rejected,
                rightIconItems
              }
            });
            setRejectedFiles(newRejectedReferences);
          }
        }, []);