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:
- 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;
- 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