I'm making a crud application using MERN stack to practice my skills cause i'm new to this I'm trying to create a new product with an image in it and i use multer to handle the images i did my backend well and tested it on Postman and it works fine, but when i pass the data to the frontend (React and Redux) it also works fine but instead of showing me the success or error message with toastfy, the browser redirect me to a new page with json data i sent
PS. i tried formik to handle my form state and errors and it didn't redirect me to the json page and it showed the errors messages i'm sending from the backend in the toast, but didn't work for me cause formik is really complicated working with files so i did it the simple way
- i use multer to handle images
- react for the frontend
- redux as a store for react
- react-toastify to show success and error messages
// multer middleware to handle the image
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "./uploads/");
},
filename: function (req, file, cb) {
cb(null, file.originalname);
}
});
const fileFilter = (req, file, cb) => {
// reject a file
if (file.mimetype === "image/jpeg" || file.mimetype === "image/png") {
cb(null, true);
} else {
cb(null, false);
}
};
const upload = multer({ storage, fileFilter });
and here's the validation and working with the backend and it's an array of methods
const createProduct = [
//validate product that it's not empthy
// then sanitize it with trima and escape
body("name")
.isLength({ min: 5 })
.withMessage("Must be at least 2 letters")
.trim()
.escape(),
body("description")
.isLength({ min: 20 })
.withMessage("Must be at least 10 letters")
.trim()
.escape(),
body("category")
.isLength({ min: 1 })
.withMessage("This field is required")
.trim()
.escape(),
body("price")
.isLength({ min: 1 })
.withMessage("Must be at least 1 number")
.isNumeric()
.withMessage("Must be Numeric")
.trim()
.escape(),
body("numberInStock")
.isLength({ min: 1 })
.withMessage("Must be at least 1 number")
.isNumeric()
.withMessage("Must be Numeric")
.trim()
.escape(),
// continue process after validation
(req, res, next) => {
console.log(req.file);
// get the validation errors from the request
const errors = validationResult(req);
// create new product after being validated and sanitized
let newProduct = new Product({
name: req.body.name,
description: req.body.description,
category: req.body.category,
price: req.body.price,
numberInStock: req.body.numberInStock,
productImage: req.file.path
});
// if there are validation errors send them as json
if (!errors.isEmpty()) {
//i only show the first error
let field = errors.errors[0].param;
let message = errors.errors[0].msg;
let errorMessage = field + " " + message;
res.status(400).json({
message: errorMessage
});
} else {
newProduct.save(function (err, product) {
if (err) {
console.log(err);
res.json({
message: "Couldn't create please try again"
});
} else {
res.redirect({
message: "Created Successfully",
product
});
}
});
}
}
];
handling the route
router.post("/api/product/create",upload.single("productImage"), createProduct);
//add product action
export const addProduct = product => dispatch => {
return new Promise((resolve, reject) => {
axios
.post("/api/product/create", product)
.then(res => {
let newProduct = res.data.product;
let successMessage = res.data.message;
dispatch(addProductSuccess(newProduct, successMessage));
resolve(successMessage);
})
.catch(error => {
console.log(error.message);
dispatch(addProductFailure(error));
reject(error.message);
});
});
};
const addProductSuccess = (product, successMessage) => {
return {
type: ADD_PRODUCT_SUCCESS,
payload: {
product,
successMessage
}
};
};
const addProductFailure = error => {
return {
type: ADD_PRODUCT_FAILURE,
payload: {
error
}
};
};
//Add product form
function AddProductForm() {
// importing categories and laoding state from out store
const { categories, error, loading } = useSelector(
state => state.categoriesss
);
const [state, setState] = useState({
name: "",
description: "",
category: "",
price: "",
numberInStock: "",
productImage: ""
});
const handleChange = e => {
const { name, value } = e.target;
setState(prevState => ({
...prevState,
[name]: value
}));
};
// react redux method to dispatch our functions
const dispatch = useDispatch();
// fetch all the the categories with dispatch before our component render
useEffect(() => {
dispatch(fetchCategories());
}, [dispatch]);
// handle submit our form
const handleSubmitt = product => {
dispatch(addProduct(product))
.then(res => {
toast.success(res, {
position: toast.POSITION.BOTTOM_LEFT,
transition: Slide
});
})
.catch(err => {
toast.error(err, {
position: toast.POSITION.BOTTOM_LEFT,
autoClose: false
});
});
};
let newProduct = {
name: state.name,
description: state.description,
category: state.category,
price: state.price,
numberInStock: state.numberInStock,
productImage: state.productImage
};
return (
<Container>
<form
action='/api/product/create'
method='post'
enctype='multipart/form-data'
onSubmit={() => handleSubmitt(newProduct)}>
<div className='form-group'>
<input
className='form-control mb-2'
type='text'
placeholder='Enter Product name'
name='name'
required
onChange={handleChange}
/>
</div>
<div className='form-group'>
<input
className='form-control mb-2'
as='textarea'
placeholder='Enter Product description'
name='description'
required
onChange={handleChange}
/>
</div>
<div className='form-group'>
<select
className='custom-select mb-2'
name='category'
required
onChange={handleChange}>
{loading && <option>loading...</option>}
{categories.map(cat => {
return (
<option key={cat._id} value={cat._id}>
{cat.name}
</option>
);
})}
</select>
</div>
<div className='form-group'>
<input
className='form-control mb-2'
type='text'
placeholder='Enter Product price'
name='price'
required
onChange={handleChange}
/>
</div>
<div className='form-group'>
<input
className='form-control mb-2'
type='text'
placeholder='Enter Product numberInStock'
name='numberInStock'
required
onChange={handleChange}
/>
</div>
<div className='form-group'>
<input
className='custom custom-file mb-2'
type='file'
name='productImage'
required
onChange={handleChange}
/>
</div>
<Button
variant='primary'
type='submit'
onClick={() => handleSubmitt(newProduct)}>
Submit{" "}
</Button>{" "}
</form>
</Container>
);
}
export default AddProductForm;