MERN Stack - Search box returning empty array

Hi. I'm developing a MERN stack web app, and am encountering a problem with my search box where if keywords provided in the search box do not match any product in the database, array returns [], whereas it is supposed to throw an error and therefore trigger react toastify for a 'product not found' notification. I'm unsure whether the problem is in the front or backend part of the project, but I provided all of the files I thought were of use. If anyone could help, I'd be very grateful. Let me know if you would like to see another file that is not included here: https://gist.github.com/deahlt/dbb9f613271f74f59be69b9f5172e847
Gist
When keywords provided in the search box do not match any product i...
When keywords provided in the search box do not match any product in the database, array returns [], whereas it is supposed to throw an error and therefore trigger react toastify for a 'pro...
41 Replies
Joao
Joao9mo ago
The problem seems to be that you are returning a 404 response when no products are found. In order to show the toast, you are checking if the response is ok in SearchBox.jsx, but response.okchecks if the response status code falls between the 200-209 range: https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
if (response.ok) {
if (Array.isArray(data.products) && data.products.length === 0) {
// Display a toast notification if no products are found
toast.error("No products found with this keyword", {
position: toast.POSITION.TOP_CENTER,
});
} else {
// Handle the search results if products are found
console.log("Search results:", data.products);
}
} else {
// Handle other response statuses if needed
console.error("Error searching products:", data.message);
}
} catch (error) {
console.error("Error searching products:", error);
toast.error("Error searching products. Please try again later.");
}
if (response.ok) {
if (Array.isArray(data.products) && data.products.length === 0) {
// Display a toast notification if no products are found
toast.error("No products found with this keyword", {
position: toast.POSITION.TOP_CENTER,
});
} else {
// Handle the search results if products are found
console.log("Search results:", data.products);
}
} else {
// Handle other response statuses if needed
console.error("Error searching products:", data.message);
}
} catch (error) {
console.error("Error searching products:", error);
toast.error("Error searching products. Please try again later.");
}
Bear in mind that an HTTP response with zero results does not necessarily imply a failure on the client. The search was ran successfully, it simply didn't yield any results. What I'd recommend is simply run your actual error checking inside an if statement for when the response didn't go well, where you would abort or return early. Then the "happy path" of the function where everything would work fine can continue. This would clean all of those deeply nested levels of if and try/catch statements...
if (! response.ok) {
// Server unavailable, no internet connection, etc
throw new Error('Something went wrong');
}

if (data.prodcuts.length === 0) {
toast.error("No products found with this keyword", {
position: toast.POSITION.TOP_CENTER,
});
} else {
// Handle search results.
}
if (! response.ok) {
// Server unavailable, no internet connection, etc
throw new Error('Something went wrong');
}

if (data.prodcuts.length === 0) {
toast.error("No products found with this keyword", {
position: toast.POSITION.TOP_CENTER,
});
} else {
// Handle search results.
}
Perhaps even something like this:
try {
if (! response.ok) {
throw new Error('Something went wrong');
}

if (data.products.length === 0) {
throw new Error('No results');
}

// Handle successfult case with results

} catch (error) {
toast.error(error.message,
{ position: toast.POSITION.TOP_CENTER }
);
}
try {
if (! response.ok) {
throw new Error('Something went wrong');
}

if (data.products.length === 0) {
throw new Error('No results');
}

// Handle successfult case with results

} catch (error) {
toast.error(error.message,
{ position: toast.POSITION.TOP_CENTER }
);
}
This way you'd show the toast notification even when there's a network issue or whatever. But be careful if you do this though, as your server might return an error including sensitive data so you might want to filter that first.
ἔρως
ἔρως9mo ago
alternativelly, send the http code 204 and DONT SEND ANY CONTENT you can use the response.status to check if it is 204 https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204 https://developer.mozilla.org/en-US/docs/Web/API/Response/status yes, the 204 means "no content", and you can send the response a lot quicker since you don't send any additional data
Joao
Joao9mo ago
I thought it was implied but you don't have to send any message at all, just return the results of the query:
const products = await Product.find({
name: { $regex: new RegExp(keyword, "i") }
});

//if (products.length === 0) {
// return res
// .status(404)
// .json({ message: "No products found with this keyword" });
//}

res.json(products);
const products = await Product.find({
name: { $regex: new RegExp(keyword, "i") }
});

//if (products.length === 0) {
// return res
// .status(404)
// .json({ message: "No products found with this keyword" });
//}

res.json(products);
The front-end will deal with displaying the appropriate information.
Metanoia
Metanoia9mo ago
with the code changes you suggested, I am still failing to receive a react toast. all searches, existent and nonexistent products, return a 200 ok status these are the files as of now:
import React from "react";
import { useState } from "react";
import { Form, Button } from "react-bootstrap";
import { useParams, useNavigate } from "react-router-dom";
import { toast } from "react-toastify";

const SearchBox = () => {
const navigate = useNavigate();
const { keyword: urlKeyword } = useParams();
const [keyword, setKeyword] = useState(urlKeyword || "");

const searchProducts = async (keyword) => {
try {
const response = await fetch(`/api/products/search?keyword=${keyword}`);
const data = await response.json();

if (!response.ok) {
throw new Error("Something went wrong");
}

if (data.products.length === 0) {
throw new Error("No results");
}

// Handle successfult case with results
} catch (error) {
toast.error(error.message, { position: toast.POSITION.TOP_CENTER });
}
};

const submitHandler = (e) => {
e.preventDefault();
if (keyword.trim()) {
setKeyword("");
searchProducts(keyword);
navigate(`/search/${keyword}`);
} else {
// navigate("/");
toast.error("Please enter a search keyword", {
position: toast.POSITION.TOP_CENTER,
});
}
};

return (
<Form onSubmit={submitHandler} className="d-flex">
<Form.Control
type="text"
name="q"
onChange={(e) => setKeyword(e.target.value)}
value={keyword}
placeholder="Search Products..."
className="mr-sm-2 ml-sm-5"
></Form.Control>
<Button
type="submit"
variant="outline-light"
className="p-2 mx-2"
// onClick={handleSearch}
>
Search
</Button>
</Form>
);
};

export default SearchBox;
import React from "react";
import { useState } from "react";
import { Form, Button } from "react-bootstrap";
import { useParams, useNavigate } from "react-router-dom";
import { toast } from "react-toastify";

const SearchBox = () => {
const navigate = useNavigate();
const { keyword: urlKeyword } = useParams();
const [keyword, setKeyword] = useState(urlKeyword || "");

const searchProducts = async (keyword) => {
try {
const response = await fetch(`/api/products/search?keyword=${keyword}`);
const data = await response.json();

if (!response.ok) {
throw new Error("Something went wrong");
}

if (data.products.length === 0) {
throw new Error("No results");
}

// Handle successfult case with results
} catch (error) {
toast.error(error.message, { position: toast.POSITION.TOP_CENTER });
}
};

const submitHandler = (e) => {
e.preventDefault();
if (keyword.trim()) {
setKeyword("");
searchProducts(keyword);
navigate(`/search/${keyword}`);
} else {
// navigate("/");
toast.error("Please enter a search keyword", {
position: toast.POSITION.TOP_CENTER,
});
}
};

return (
<Form onSubmit={submitHandler} className="d-flex">
<Form.Control
type="text"
name="q"
onChange={(e) => setKeyword(e.target.value)}
value={keyword}
placeholder="Search Products..."
className="mr-sm-2 ml-sm-5"
></Form.Control>
<Button
type="submit"
variant="outline-light"
className="p-2 mx-2"
// onClick={handleSearch}
>
Search
</Button>
</Form>
);
};

export default SearchBox;
import express from "express";
import Product from "../models/productModel.js";

const router = express.Router();

router.get("/search", async (req, res) => {
const { keyword } = req.query;

try {
// Query the database for products that match the keyword
const products = await Product.find({
name: { $regex: new RegExp(keyword, "i") },
});

//if (products.length === 0) {
// return res
// .status(404)
// .json({ message: "No products found with this keyword" });
//}

res.json(products);
} catch (error) {
console.error("Error searching products:", error);
res.status(500).json({ message: "Internal server error" });
}
});

export default router;
import express from "express";
import Product from "../models/productModel.js";

const router = express.Router();

router.get("/search", async (req, res) => {
const { keyword } = req.query;

try {
// Query the database for products that match the keyword
const products = await Product.find({
name: { $regex: new RegExp(keyword, "i") },
});

//if (products.length === 0) {
// return res
// .status(404)
// .json({ message: "No products found with this keyword" });
//}

res.json(products);
} catch (error) {
console.error("Error searching products:", error);
res.status(500).json({ message: "Internal server error" });
}
});

export default router;
@ἔρως do you have any suggestion?
Joao
Joao9mo ago
What is the actual data returned? It may be data.length instead of data.products.length
Metanoia
Metanoia9mo ago
No description
Metanoia
Metanoia9mo ago
it wont make a difference let's say toastify is giving me an issue. i can honestly scrape that out and simply have it say as an h3 tag or something below the 'discover products' that no product matches the search. do i have to follow the same logic for that?
Joao
Joao9mo ago
Yes, same process all you care about is react to different scenarios. Are you able to log something to the console when you receive 0 results? Both inside the if statement and after throwing an error in the catch statement. If not, this is where having a reproducible build of the app would be helpful so that we can test on our end as well
Metanoia
Metanoia9mo ago
i'll deploy my site on render in a second, if that helps
Joao
Joao9mo ago
Uploading the code as a repository might be better so that we can clone the project and deploy locally.
Metanoia
Metanoia9mo ago
okay, ill share in a min
Metanoia
Metanoia9mo ago
I apologize for the delay. https://github.com/deahlt/shop
GitHub
GitHub - deahlt/shop
Contribute to deahlt/shop development by creating an account on GitHub.
Joao
Joao9mo ago
No worries, give me a sec I'll take a look
Metanoia
Metanoia9mo ago
was it helpful?
Joao
Joao9mo ago
Yeah sorry, I've actually run into something but I'm now looking into this
Joao
Joao9mo ago
For now I've noticed that the toast notification works fine when there's no internet connection:
No description
Metanoia
Metanoia9mo ago
so that means that the front is handling it okay i guess?
Joao
Joao9mo ago
It seems like it, but I need to test the no results case yet. I'm running into the good old classing CORS issue though 😄 Other errors also work fine: Are you using MongoDB Atlas or runnign your own MongoDB instance? Mmm ok I think I've got it
Metanoia
Metanoia9mo ago
Atlas
Joao
Joao9mo ago
The issue is that your routes are setup like this:
app.use("/api/products/search", searchRoutes);
app.use("/api/products/search", searchRoutes);
But the route itself responds to "/search"|. So you would have to send a request to /api/products/search/search?q=keyword can you double check that?
Metanoia
Metanoia9mo ago
so i should modify that line with the new url?
Joao
Joao9mo ago
No description
Joao
Joao9mo ago
I would prefer you modified the route on the backend, and set it such that the "searchRoutes" route is simply "/"
Joao
Joao9mo ago
No description
Joao
Joao9mo ago
I've removed all the actual searching because I didn't want to set it up but it doesn't matter, this was just to test the empty array result. Notice the route is now "/" because it will already be prefixed when it's wired up in server.js But in any case, the toast notification is working properly so I'm guessing there's something going on with the routing. Aside from this one file, another change that might come in handy is adding the cors package and use that in server.js. This will help with CORS issues, and eventually you'll want to use it anyway when you deploy in production as a security precaution. https://expressjs.com/en/resources/middleware/cors.html
Metanoia
Metanoia9mo ago
so, just to be clear, ive set it to
app.use("/", searchRoutes);
app.use("/", searchRoutes);
and the search routes to
router.get("/", async (req, res) => {
const { keyword } = req.query;

try {
// Query the database for products that match the keyword
const products = await Product.find({
name: { $regex: new RegExp(keyword, "i") },
});

res.json(products);
} catch (error) {
console.error("Error searching products:", error);
res.status(500).json({ message: "Internal server error" });
}
});
router.get("/", async (req, res) => {
const { keyword } = req.query;

try {
// Query the database for products that match the keyword
const products = await Product.find({
name: { $regex: new RegExp(keyword, "i") },
});

res.json(products);
} catch (error) {
console.error("Error searching products:", error);
res.status(500).json({ message: "Internal server error" });
}
});
. is this the correct approach? becuase even as this, toast still doesnt show :/ tysm
Joao
Joao9mo ago
No, only in searchRoutes.js, leave server.js alone
Metanoia
Metanoia9mo ago
okay, it still doesn't improve
Joao
Joao9mo ago
Mmm ok I think then the issue is with the order of the routes:
-app.use("/api/products", productRoutes);
app.use("/api/users", userRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);
app.use("/api/products/search", searchRoutes);
+app.use("/api/products", productRoutes);
-app.use("/api/products", productRoutes);
app.use("/api/users", userRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);
app.use("/api/products/search", searchRoutes);
+app.use("/api/products", productRoutes);
Try that The more specific route should come first.
Metanoia
Metanoia9mo ago
yay! it now toasts 'no results' i would've never thought of this lol
Joao
Joao9mo ago
Yeah one of those things 😄 It seemed off at first but didn't think of it either until now. I just removed all the code that wasn't relevant to me for the test so that's how it worked fine for me
Metanoia
Metanoia9mo ago
thank you! i appreciate your help greatly
Metanoia
Metanoia9mo ago
i have kind of a related question if you have the time / interest. so when i search for a product, im redirected to the /search/keyword route and visually i recieve this (which is the same as the homepage minus the carousel) because i dont have a separate screen for the search. when i search for something that isn't present, that card vanishes and im left with an empty screen with just the "discover products" heading. do you think i could, in case of no result, add some ui to show a generic blank page or something just so it doesnt look super empty
No description
Metanoia
Metanoia9mo ago
that is without creating an entirely new screen for it
Joao
Joao9mo ago
You could, yes. Personally I would find it a bit better in terms of UI. The toast notification is nice to let me know there are no results but the lack of results kind of already tells me that as well 😄 Maybe an image filling that blank space would be more suited. But you should probably create a search page anyway for that, regardless of whether there are elements or not.
Metanoia
Metanoia9mo ago
so you believe it is better i move all this to a separate page?
Joao
Joao9mo ago
It's up for debate if it's better, but I think it is. Either that, or don't redirect when there are no results.
Metanoia
Metanoia9mo ago
how would i go about that
Joao
Joao9mo ago
In the searchBox submit handler you are currently navigating regardless of the outcome. You could do this as part of the searchProducts function, once you've confirmed there are results. If you get an error or are no results, only the toast would show up.
Metanoia
Metanoia9mo ago
hi. i'm sorry to be bothering you again, i was just in need of some help with adding functionality because i feel like im not understanding react or node very well (my entire project is based on a course, and im doing it for school). would you mind helping me or providing examples on how to implement sorting and filtering my list of products? ive been asking chatgpt for assistance but i fear it isn't a talented coder 😅
Joao
Joao9mo ago
I'm not a big fan of ChatGPT in general for learning purposes. It's no different than copying code from Stack Overflow or some blog post. Even if it did give you the right answer, you wouldn't learn much. As a general rule of thumb, asking people would help you even more for two reasons: it forces you to express your problem in a way that other people can understand, and you will likely receive feedback on other areas that you havent' asked about like for example how I did with the cors package earlier. But sure, let's jump into the sorting and filtering. Sorting and filtering are operations that are very efficient when done by the database itself so it's best to focus on that one. I don't exactly remember the syntax as it's been a while since I used Mongoose but checking the documentation should be pretty easy All right so filtering is something you'd do with the find method, just add more things to "find by" other than the keyword as needed. Sorting is part of the same method, provided as an object as per the documentation: https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions() https://mongoosejs.com/docs/api/model.html#Model.find() So you can do something like
Product.find(keyword).sort({ someKey: -1 });
Product.find(keyword).sort({ someKey: -1 });
The rules of sorting are defined by MongoDB: https://www.mongodb.com/docs/manual/reference/method/cursor.sort/ Like I said I haven't used it in a while but if you can give an example of what you're after maybe we can come up with something. Although I still encourage you to read through these and try it first. Based on an example I had you have to chain the method after find. The someKey is part of the object returned and then you can use one of the following values:
If an object is passed, values allowed are asc, desc, ascending, descending, 1, and -1.
For example:
Product.find(keyword).sort({ createdAt: 'asc' });
Product.find(keyword).sort({ name: 'dec' });
Product.find(keyword).sort({ price: 'asc' });
Product.find(keyword).sort({ createdAt: 'asc' });
Product.find(keyword).sort({ name: 'dec' });
Product.find(keyword).sort({ price: 'asc' });
Want results from more Discord servers?
Add your server