
Join the Conversation!
Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.
In the previous lesson, we created a server action to create a product. In this lesson, we will create a server action to read a product.
Let's start by creating a new server action to read a product. We will use the findById method from Mongoose to read a product from the database.
Let's open our file in our folder and add the following code:
products.ts;
export async function getProductById() {
// read a product from the database
}
Before we can find our product, we need to know how to identify it. In our MongoDB database, each product has a unique identifier called an . We can use this identifier to find our product.
Let's update our file to accept an parameter:
products.ts;
export async function getProductById(_id: string) {
// read a product from the database
}
Now, let's use the findById method from Mongoose to read a product from the database. We will pass the parameter to the findById method to identify the product we want to read.
products.ts;
export async function getProductById(_id: string) {
await dbConnect();
const product = await Product.findById(_id);
if (!product) {
return null;
}
return product;
}
In the code above, we use the findById method from Mongoose to find a product with the given . If the product is not found, we return . Otherwise, we return the product.
When working with databases, it's important to handle errors properly. Let's add error handling to our server action to read a product.
products.ts;
export async function getProductById(_id: string) {
await dbConnect();
try {
const product = await Product.findById(_id);
if (!product) {
return null;
}
return product;
} catch (error) {
console.error(error);
return null;
}
}
Now that we have created the server action, let's integrate it into our application. Open up the file to see the component that displays the Product details.
Currently, the Product component just displays some placeholder data. We want to replace that placeholder data with the actual Product data from the database.
Let's pass in a "product" to the Product component. We'll fetch the Product from the database using the server action and pass it to the Product component.
Product.tsx
// import the product type from Prisma
import Product from "@/lib/models/product";
import Stars from "@/components/product/Stars";
import ImageDisplay from "@/components/product/ImageDisplay";
// Allow the Product component to accept a product as a prop
export default async function ProductView({ product }: { product: Product }) {
return (
<div className="grid gap-6">
<ImageDisplay />
<div className="grid gap-2">
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-gray-500 dark:text-gray-400">
{product.description}
</p>
<div className="flex items-center gap-4">
<span className="text-4xl font-bold">${product.price}</span>
<div className="flex items-center gap-0.5">
<Stars rating={4} />
</div>
</div>
</div>
</div>
);
}
Next we'll need to pass that Product to the Product component. Open up the file to see how the Product component is used.
We're already pulling in the params and grabbing the 'id', so let's use that ID to fetch the Product from the database and pass it to the Product component.
//
import { getProductById } from "@/lib/actions/products";
export default async function Page({ params }: { params: { path: string[] } }) {
...
const product = await getProductById(id);
if (!product) {
return <div>Product not found</div>;
}
return (
<div className="...">
<Product product={product} />
...
);
}
This is a common pattern. We'll often need to pass data from the URL to a component. In this case, we're passing the Product ID from the URL to the Product component.
We want to be able to navigate to different Product pages based on the Product ID in the URL. By passing the Product ID from the URL to the Product component, we can fetch the Product from the database and display its details.
We also need to populate the images for the Product. Let's update the component to display the images for the Product.
ImageDisplay.tsx
// modify the props to accept an array of strings
export default function ImageDisplay({ images }: { images: string[] }) {
return (
...
<CarouselContent>
{/* map over the images */}
{images.map((image, index) => (
<CarouselItem key={index}>
<img
src={image}
alt="product"
className="w-full h-full object-cover"
/>
</CarouselItem>
))}
</CarouselContent>
...
);
}
Now we need to pass the images to the component. Let's update the component to pass the images to the component.
Product.tsx
export default async function ProductView({ product }: { product: Product }) {
return (
<div className="grid gap-6">
<ImageDisplay images={product.images} />
...
</div>
);
}
We also want to display the rating and reviews for the Product. Let's create a file called in the folder to handle the ratings and reviews.
We'll import the model from the file and create a server action to get the reviews for a Product.
import Review from "@/lib/models/review";
reviews.ts;
("use server");
async function getReviewsAndRating(productId: string) {
await dbConnect();
// grab all the reviews for the product
const reviews = await Review.find({ productId });
// TODO calculate the average rating
const averageRating = undefined; // TODO
return { reviews, averageRating };
}
Grabbing the reviews should be straightforward. We just need to find all the reviews for the Product with the given Product ID.
Because we're using references, we can use the field in the model to find all the reviews for a Product.
To calculate the average rating, we'll use an aggregate query. We'll calculate the average rating by using the operator in the aggregate query.
// import the mongoose library
// so we can coerce the productId to an ObjectId
import mongoose from "mongoose";
reviews.ts;
async function getReviewsAndRating(productId: string) {
await dbConnect();
// grab all the reviews for the product
const reviews = await Review.find({ productId });
// calculate the average rating
const averageRatingResult = await Review.aggregate([
// only grab records with the given productId
{ $match: { productId: new mongoose.Types.ObjectId(productId) } },
// group the records and return an average rating
{ $group: { _id: null, average: { $avg: "$rating" } } },
]);
const averageRating = averageRatingResult[0]?.average || 0;
return { reviews, averageRating };
}
In the aggregate query, we first use the operator to filter the records by the .
We use the method to convert the to an ObjectId.
Next, we use the operator to group the records and calculate the average rating using the operator.
Finally, we extract the average rating from the result of the aggregate query.
Aggregates are complex subjects, but familiar if you've worked with databases before. If you're curious to learn more or want a more in depth explanation of aggregates right now- check out
This future lesson on Aggregates
We could calculate the average rating by iterating over the reviews and summing up the ratings. We could then divide the sum by the number of reviews to get the average rating.
reviews.ts;
async function getReviewsAndRating(productId: string) {
await dbConnect();
// grab all the reviews for the product
const reviews = await Review.find({ productId });
// calculate the average rating
let totalRating = 0;
reviews.forEach((review) => {
totalRating += review.rating;
});
const averageRating = reviews.length > 0 ? totalRating / reviews.length : 0;
return { reviews, averageRating };
}
Often when we are already grabbing the necessary data, it's more efficient to calculate the average rating in the application code rather than using an aggregate query.
Now that we have created the server action, let's integrate it into our application. Open up the and update the file to fetch the reviews and rating for the Product.
Right alongside the call, let's also fetch the reviews and rating for the Product.
//
const product = await findProductById(id);
const { reviews, averageRating } = await getReviewsAndRating(id);
if (!product) {
return <div>Product not found</div>;
}
return (
<div className="...">
// add the reviews and averageRating to the Product component
<Product product={product} rating={averageRating} />
...
);
Let's update the component to display the rating and reviews for the Product.
Product.tsx
import Stars from "@/components/product/Stars";
import ImageDisplay from "@/components/product/ImageDisplay";
export default async function ProductView({ product, rating }: { product: Product, rating: number }) {
return (
<div className="grid gap-6">
<ImageDisplay images={product.images} />
<div className="grid gap-2">
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-gray-500 dark:text-gray-400">
{product.description}
</p>
<div className="flex items-center gap-4">
<span className="text-4xl font-bold">${product.price}</span>
<div className="flex items-center gap-0.5">
<Stars rating={rating} />
</div>
</div>
</div>
</div>
);
}
Nice. Now, back in the file, we can see that reviews are just a list of placeholder blocks.
Given that the results from our server action is an array of reviews, can you think of a way to display the reviews?
We can map over the reviews and display each review using the component.
app/product/[[...path]].tsx
...
return (
<div className="...">
<Product product={product} rating={averageRating} />
<div className="grid gap-4">
{reviews.map((review, index) => (
<Review key={index} review={review} />
))}
</div>
</div>
);
Let's update the component to display the review details.
Let's open up the file and update the component to display the review details.
//
import Review from "@/lib/models/review";
export default function ReviewDisplay({ review }: { review: Review }) {
// calculate the initials of the author
const initials = review.author.name
.split(" ")
.map((n) => n[0])
.join("");
return (
<Card>
<CardContent className="grid gap-4 p-4">
<div className="flex items-center gap-4">
<Avatar>
<AvatarImage alt="@jaredpalmer" src="/placeholder-avatar.jpg" />
// display the initials of the author
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<div>
// display the author's name and rating
<h3 className="font-semibold">{review.author.name}</h3>
<div className="flex items-center gap-0.5">
<Stars rating={review.rating} />
</div>
</div>
</div>
// display the review content
<p>{review.content}</p>
</CardContent>
</Card>
);
}
In the Review component, we calculate the initials of the author by splitting the author's name and taking the first letter of each word.
We display the author's name and rating using the and fields.
Remember, we used an embedded document to store the author's details in the Review model, so we can access the author's name and rating directly from the review object.
The review object looks like this:
//
{
author: {
name: "John Doe",
email: "email@jd.com",
},
rating: 4,
content: "This is a great product!",
_id: "123",
productId: "456",
}
So we can access the author's name and rating using and .
In this lesson, we created a server action to read a product from the database. We used the method from Mongoose to find a product by its .
We added an aggregate query to calculate the average rating for a product based on its reviews. We also displayed the product images, rating, and reviews in the Product component.
In the next lesson, we will create a server action to update a product.
"Please login to view comments"
Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.