Conflicting React State Issue with Multiple Image Upload Forms in MERN Stack App

Description:
I am encountering an issue in my MERN stack application where I have two components, CategoryForm and ProductForm, each containing an image upload form. The problem is that when I upload an image in the CategoryForm, it ends up being uploaded for the product instead.

Code Snippets:

  1. CategoryForm:
import React, { useState } from "react";
import {
  createCategory,
  getAllCategories,
  getCategoryById,
} from "../api/functions/categories";
import { addCategory, setCategories } from "../redux/categorySlice";
import { useDispatch, useSelector } from "react-redux";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import DeleteCategoryModel from "./DeleteCategoryModel";
import showToast from "./showToast";

const CategoryForm = () => {
  const dispatch = useDispatch();
  const categories = useSelector((state) => state.category.categories);

  const [categoryObject, setCategoryObject] = useState({});
  const [imageCategoryName, setImageCategoryName] = useState(null);
  const [selectedCategoryFile, setSelectedCategoryFile] = useState(null);

  const [formCategory, setFormCategory] = useState({
    categoryName: "",
    categoryImage: null,
  });

  const handleInputChange = ({ target }) => {
    setFormCategory({
      ...formCategory,
      categoryName: target.value,
    });
  };

  const handleFileChange = ({ target }) => {
    const file = target.files[0];

    // console.log("handleFileChange category => ", file);

    if (file) {
      setSelectedCategoryFile(file);
      setImageCategoryName(file.name);
      setFormCategory({
        ...formCategory,
        categoryImage: file,
      });
    }
  };

  const getAllCategoriesData = async () => {
    try {
      const response = await getAllCategories();

      if (response.status === 200) {
        dispatch(setCategories(response.data.categories));
      }
    } catch (error) {
      console.error("Error fetching categories:", error);
    }
  };

  const handleAddCategory = async (e) => {
    e.preventDefault();

    const formDataToSend = new FormData();
    formDataToSend.append("name", formCategory.categoryName);
    formDataToSend.append("image", formCategory.imageCategory);

    try {
      const response = await createCategory(formDataToSend);

      if (response.status === 201) {
        getAllCategoriesData();
        dispatch(addCategory(response.data.category));
        showToast(response.data.message.en, "info");
      }
    } catch (error) {
      if (error.response.status === 500 || error.response.status === 400) {
        showToast(error.response.data.message, "error");
      }

      // reset data
      setImageCategoryName(null);
      setSelectedCategoryFile(null);
      setFormCategory({
        categoryName: "",
        imageCategory: null,
      });

      console.error("error.response.data.message", error.response.data.message);
    }
  };

  const handleCancel = () => {
    setImageCategoryName(null);
    setSelectedCategoryFile(null);
    setFormCategory({
      categoryName: "",
      imageCategory: null,
    });
  };

  const customTableColumns = [
    "table-info",
    "table-warning",
    "table-danger",
    "table-success",
    "table-primary",
  ];

  const getCategoryObject = async (e, id) => {
    e.preventDefault();

    try {
      const res = await getCategoryById(id);
      if (res.status === 200) {
        setCategoryObject(res.data.category);
      }
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <>
      <DeleteCategoryModel
        category={categoryObject}
        fetchAllCategories={getAllCategoriesData}
      />

      <form onSubmit={handleAddCategory} className="mb-4">
        <div className="form-group">
          {selectedCategoryFile ? (
            <div className="file-area file-area_empty uploader-product-media-file-area uploader-product-media-file-area_type_presentational">
              <img
                src={URL.createObjectURL(selectedCategoryFile)}
                alt="Selected"
                className="mt-2"
                style={{ maxWidth: "100%", maxHeight: "150px" }}
              />
              <input
                className="file-area__input"
                id="uploader-product-presentational-media-input-0"
                type="file"
                accept="image/*"
                onChange={handleFileChange}
              />
            </div>
          ) : (
            <div className="file-area file-area_empty uploader-product-media-file-area uploader-product-media-file-area_type_presentational">
              <label
                className="file-area__label"
                htmlFor="uploader-product-presentational-media-input-0"
              >
                <input
                  className="file-area__input"
                  id="uploader-product-presentational-media-input-0"
                  type="file"
                  accept="image/*"
                  onChange={handleFileChange}
                />
                <span className="uploader-product-media-file-area__label">
                  <svg
                    viewBox="0 0 40 40"
                    xmlns="http://www.w3.org/2000/svg"
                    className="uploader-product-media-file-area__icon uploader-product-media-file-area__icon_size_medium icon_picture-set icon_picture"
                    width="20"
                    height="20"
                  >
                    <path
                      d="M3.098 10l-.815-3.333H37.71L36.892 10H3.098zm2.635-6.667L5.002 0h29.99l-.732 3.333H5.733zM40 30H0l3.333 10h33.334L40 30zM5.173 26.667l-1.356-10h32.351l-1.398 10h3.367L40 13.333H0l1.808 13.334h3.365zm4.64-6.659c-.081-.925.699-1.675 1.739-1.675 1.041 0 1.925.749 1.975 1.674.05.925-.73 1.675-1.74 1.675-1.009 0-1.894-.749-1.974-1.674zm12.625-.373l-3.04 4.467-3.021-2.187-4.71 4.752h16.666l-5.895-7.032z"
                      fillRule="evenodd"
                    ></path>
                  </svg>
                  <strong className="t3">
                    {imageCategoryName
                      ? imageCategoryName
                      : "Upload presentational image"}
                  </strong>
                </span>
              </label>
            </div>
          )}
        </div>
        <div className="form-group">
          <input
            type="text"
            name="name"
            required
            className="form-control"
            placeholder="e.g., Salon"
            onChange={handleInputChange}
          />
        </div>

        <div className="form-group">
          <textarea
            rows="3"
            className="form-control"
            name="description"
            onChange={handleInputChange}
            placeholder="description"
          ></textarea>
        </div>

        <div className="d-flex align-items-center gap-3">
          <button type="submit" className="btn btn-primary">
            Add Category
          </button>
          <button type="reset" className="btn btn-light" onClick={handleCancel}>
            Cancel
          </button>
        </div>
      </form>

      <table className="table table-bordered">
        <thead>
          <tr>
            <th>#</th>
            <th>Name</th>
            <th>delete</th>
          </tr>
        </thead>
        <tbody>
          {categories?.map((category, index) => (
            <tr className={customTableColumns[index]} key={category?._id}>
              <td>{index + 1}</td>
              <td>{category?.name?.en}</td>
              <td>
                <button
                  className="btn btn-danger"
                  data-bs-toggle="modal"
                  data-bs-target="#deleteCategory"
                  onClick={(e) => getCategoryObject(e, category._id)}
                >
                  <FontAwesomeIcon icon={faTrash} />
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
};

export default CategoryForm;
  1. ProductForm:
import React, { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { createProduct } from "../api/functions/products";
import { getAllCategories } from "../api/functions/categories";
import { useDispatch, useSelector } from "react-redux";

const ProductForm = ({ fetchProducts }) => {
  const dispatch = useDispatch();
  const categories = useSelector((state) => state.category.categories);

  const [imageName, setImageName] = useState(null);

  const [categoriesList, setCategoriesList] = useState(categories);
  const [selectedImageFile, setSelectedImageFile] = useState(null);

  const [formData, setFormData] = useState({
    title: "",
    description: "",
    price: 0,
    categoryId: "",
    productImage: null,
  });

  const getAllCategoriesData = async () => {
    const response = await getAllCategories();
    if (response.status === 200) {
      setCategoriesList(response.data.categories);
    }
  };

  useEffect(() => {
    getAllCategoriesData();
  });

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value,
    });
  };

  const handleFileChange = (event) => {
    const file = event.target.files[0];

    // console.log("handleFileChange product => ", file);

    if (file) {
      setSelectedImageFile(file);
      setImageName(file.name);
      setFormData({
        ...formData,
        productImage: file,
      });
    }
  };

  const onSubmitProduct = async (e) => {
    e.preventDefault();

    try {
      const formDataToSend = new FormData();
      formDataToSend.append("title", formData.title);
      formDataToSend.append("description", formData.description);
      formDataToSend.append("price", formData.price);
      formDataToSend.append("categoryId", formData.categoryId);
      formDataToSend.append("image", formData.image);

      const response = await createProduct(formDataToSend);

      if (response.status === 201) {
        fetchProducts();
        toast.info(`${response.data.message.en}`, {
          position: "bottom-right",
          autoClose: 5000,
          hideProgressBar: false,
          closeOnClick: true,
          pauseOnHover: true,
          draggable: false,
          progress: undefined,
          theme: "colored",
        });

        setFormData({
          title: "",
          description: "",
          price: 0,
          categoryId: "",
          image: null,
        });

        setImageName("");

        setSelectedImageFile(null);
      }
    } catch (error) {
      console.log(error.response);

      toast.error(`${error.response.data.message}`, {
        position: "bottom-right",
        autoClose: 5000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: false,
        progress: undefined,
        theme: "colored",
      });
    }
  };

  const resetFormData = () => {
    setImageName(null);
    setSelectedImageFile(null);
    setFormData({
      title: "",
      description: "",
      price: 0,
      categoryId: "",
      image: null,
    });
  };

  return (
    <form onSubmit={onSubmitProduct} className="forms-sample">
      {/* Product Form */}
      <div className="form-group">
        <label>
          Product Name <span className="text-danger">*</span>
        </label>
        <input
          type="text"
          className="form-control"
          name="title"
          value={formData.title}
          onChange={handleInputChange}
          maxLength={20}
          required
        />
      </div>
      <div className="form-group">
        <label>
          Description <span className="text-danger">*</span>
        </label>
        <textarea
          className="form-control"
          name="description"
          value={formData.description}
          onChange={handleInputChange}
          rows="4"
        ></textarea>
      </div>
      <div className="form-group">
        <label>
          Category <span className="text-danger">*</span>
        </label>
        <select
          className="form-control"
          name="categoryId"
          value={formData.categoryId}
          onChange={handleInputChange}
          required
        >
          <option value="" disabled>
            Select Category
          </option>
          {categoriesList?.map((category) => (
            <option key={category._id} value={category._id}>
              {category.name.en}
            </option>
          ))}
        </select>
      </div>
      <div className="form-group">
        <label>
          Price <span className="text-danger">*</span>
        </label>
        <input
          type="number"
          className="form-control"
          name="price"
          value={formData.price}
          onChange={handleInputChange}
          min={5}
          max={10000}
          required
        />
      </div>
      <div className="form-group">
        {selectedImageFile ? (
          <div className="file-area file-area_empty uploader-product-media-file-area uploader-product-media-file-area_type_presentational">
            <img
              src={URL.createObjectURL(selectedImageFile)}
              alt="Selected"
              className="mt-2"
              style={{ maxWidth: "100%", maxHeight: "150px" }}
            />
            <input
              className="file-area__input"
              id="uploader-product-presentational-media-input-0"
              type="file"
              accept="image"
              multiple=""
              onChange={handleFileChange}
            />
          </div>
        ) : (
          <div className="file-area file-area_empty uploader-product-media-file-area uploader-product-media-file-area_type_presentational">
            <input
              className="file-area__input"
              id="uploader-product-presentational-media-input-0"
              type="file"
              accept="image"
              multiple=""
              onChange={handleFileChange}
            />
            <label
              className="file-area__label"
              htmlFor="uploader-product-presentational-media-input-0"
            >
              <span className="uploader-product-media-file-area__label">
                <svg
                  viewBox="0 0 40 40"
                  xmlns="http://www.w3.org/2000/svg"
                  className="uploader-product-media-file-area__icon uploader-product-media-file-area__icon_size_medium icon_picture-set icon_picture"
                  width="20"
                  height="20"
                >
                  <path
                    d="M3.098 10l-.815-3.333H37.71L36.892 10H3.098zm2.635-6.667L5.002 0h29.99l-.732 3.333H5.733zM40 30H0l3.333 10h33.334L40 30zM5.173 26.667l-1.356-10h32.351l-1.398 10h3.367L40 13.333H0l1.808 13.334h3.365zm4.64-6.659c-.081-.925.699-1.675 1.739-1.675 1.041 0 1.925.749 1.975 1.674.05.925-.73 1.675-1.74 1.675-1.009 0-1.894-.749-1.974-1.674zm12.625-.373l-3.04 4.467-3.021-2.187-4.71 4.752h16.666l-5.895-7.032z"
                    fillRule="evenodd"
                  ></path>
                </svg>
                <strong className="t3">
                  {imageName ? imageName : "Upload presentational image"}
                </strong>
              </span>
            </label>
          </div>
        )}
        <div className="UploaderProductUploadSection__description t3 ">
          JPG, PNG formats only. File under 10MB. The main image must have an
          8:5 aspect ratio, the minimum size of 336 x 350 px. Main information
          about your item should be placed in the center of an image, it will
          look better in the ads. Please, check our.
        </div>
      </div>
      <button type="submit" className="btn btn-primary">
        Create new product
      </button>
      <button
        type="button"
        className="btn btn-secondary ml-3"
        onClick={resetFormData}
      >
        Cancel
      </button>
    </form>
  );
};

export default ProductForm;

Problem:

The root cause of the issue appears to be a state conflict between the two components. Both components use a state variable named image or categoryImage, and the uploads are getting mixed up.

Expected Behavior:
I want to be able to upload images for categories and products independently without conflicts.

What I’ve Tried:
I have already attempted renaming the state variables in one of the components, but the issue persists.

Questions:

How can I ensure that each component handles its image upload independently without conflicts?
Are there any best practices for managing state in multiple components with file uploads in a MERN stack application?

Additional Information:

React version: 18.2.0
Redux usage: 9.0.4