Search code examples
reactjsimmutability

How to push to array inside dynamic object using React Hooks?


In this small code that I've written I have created a dynamic object upon setting state and adding files to it. However, the new file overwrites the previous file. With no luck, I have tried using the spread operator inside the array brackets next to mappedFiles. It seems upon setting the state dynamically, I am not able to concatenate or push to the array inside the files object.

Here is the code...

import React, { useCallback, useState, useContext } from "react";
import { ImageUploadContext } from "../../Store";
import styles from "./commentssection.module.css";
import { useDropzone } from "react-dropzone";

function ImageUploader({ title }) {
  const [files, setFiles] = useContext(ImageUploadContext);
  const maxSize = 5048576;


 //ISSUE IS HERE. CREATING THE SETSTATE INSIDE THIS CALLBACK
  const onDrop = useCallback(
    (acceptedFiles) => {
      const mappedFiles = acceptedFiles.map((file) =>
        Object.assign(file, {
          preview: URL.createObjectURL(file),
        })
      );
      // This setstate function adds to dynamic array but doesn't return previous one. The array is being overwritten
      setFiles((state) => ({ ...state, [title]: [mappedFiles] }));
    },
    [files]
  );

  const {
    isDragActive,
    getRootProps,
    getInputProps,
    isDragReject,
    acceptedFiles,
    rejectedFiles,
  } = useDropzone({
    onDrop,
    accept: "image/*",
    minSize: 0,
    maxSize: 10000000,
  });
  console.log(files);
  const isFileTooLarge = rejectedFiles
    ? rejectedFiles.length > 0 && rejectedFiles[0].size > maxSize
    : null;
  return (
    <div>
      <p>Please include comments in notes</p>
      <hr className={styles["HR"]} />
      <form className={styles["UploadForm"]}>
        <div className={styles["UploadWrapper"]}>
          <h5>Upload photos of issues found in the {title}</h5>
          <section className={styles["Container"]}>
            <div className={styles["ImageInput"]} {...getRootProps()}>
              <input {...getInputProps()} />
              {!isDragActive && "Click here or drop a file to upload!"}
              {isDragActive && !isDragReject && "Drop it like it's hot!"}
              {isDragReject && "File type not accepted, sorry!"}
              {isFileTooLarge && (
                <div className="text-danger mt-2">File is too large.</div>
              )}
            </div>
          </section>
          <div>
            {files[title]
              ? files[title].map((object, index) =>
                  object.map((subObject, subIndex) => {
                    return (
                      <img
                        style={{ height: "80px" }}
                        className={styles["RenderedImage"]}
                        key={index}
                        src={subObject.preview}
                      />
                    );
                  })
                )
              : null}
          </div>
          <p>
            Please take a picture of any issues that you find and upload them
            here. NOTE: it is only necessary to upload pictures of problems that
            you find.
          </p>
        </div>
        <div className={styles["CommentWrapper"]}>
          <h5>Notes of the {title}</h5>
          <textarea className={styles["Textarea"]} />
        </div>
      </form>
    </div>
  );
}
export default ImageUploader;

Edit:

I was able to figure it out thanks to @lawrence-witt1 . Here is the code for the arrays to be parent component specific.

const onDrop = useCallback(
    (acceptedFiles) => {
      const mappedFiles = acceptedFiles.map((file) =>
        Object.assign(file, {
          preview: URL.createObjectURL(file),
        })
      );
      return files[title]
        ? setFiles((state) => ({
            ...state,
            [title]: [...state[title], mappedFiles],
          }))
        : setFiles((state) => ({
            ...state,
            [title]: [mappedFiles],
          }));
    },
    [files, title]
  );

Solution

  • I think I spotted the issue - you need to include title in the dependency array of onDrop:

    const onDrop = useCallback(
        (acceptedFiles) => {
          ...
        },
        [files, title]
    );
    

    Otherwise you run the risk of having a stale value for title which would overwrite your state object's property.

    Edit:

    I think this is what you were looking for:

    setFiles((state) => ({ ...state, [title]: [...state[title], ...mappedFiles]}));