In this chapter we're gonna go ahead and build the base product collection. And as you've probably noticed I did not attach any pictures here. That's because we're not gonna be building any UI just yet. We are first just going to focus on establishing the basic products collection and we are going to learn how to load products based on a specific category and we are going to utilize our prefetching in React server components along with suspense loading in the client component. We also have to refactor some components to their modules as we did last time and then push that to GitHub.
So as always confirm that you are on the master branch, confirm that your graph looks similar to this and feel free to run git status to confirm you are on the master branch. After that you can go ahead and do bun run dev. So what we're going to do now is go inside of source collections and let's create products.ts. And inside of here, I'm going to go ahead and import collection config from payload and export const products collection config. Let's give this a slug of products.
And then Let's go ahead and create some fields here. First one being the name of the product, which is a type of text. And the required should be true, of course. Then let's add the description and let's give it a type of text as well. And let's go ahead and add a price with a type of number.
This will also be required. And what I want to do now is add a relation to a category. So every single product will have to have a category. So it will be a type of relationship and the relation to categories. You can see that if you misspell, it's an error.
That is because it will read, it will expect this slug from our categories, right? And put has many to be false. So one product can only belong to one category. Besides that, let's also add a name, image with a type upload and relation to media. If you're wondering where does media come from, so media has already been defined here automatically when I added payload, right?
If yours for any reason hasn't, you can just add it, right? So create media collection as you would with any and it's just one field here and upload set to true. And so this is just if you don't have it. After that, you should go inside of the payload config and ensure that you have media added here. That's it.
And then you can have images here. And then let's go ahead and let's add a name called refund policy, for example. And let's give it a type of select. So we are now going to give the user some options. You can select whatever options you want.
I'm going to use this. So 30 day, 14 day, 7 day, 3 day, 1 day and no refunds. And let's put the default value to be 30 day. There we go. And I'm going to stop here.
We are going to have more fields later on, but I think for now this is enough. Now we have to go inside of payload.config and import products from collections products and simply add it here. Like this. And now you should have access to your new product collection. So I'm going to go ahead and refresh my localhost 3000 here.
And you should go to slash admin, right? Or you can just log in. We now have a simpler way, right? I can just log in now. And I can go to my dashboard here, which is slash admin, you might have to wait some time for this to compile.
And then you should have access to products. There we go. And when you click create new product, you will now have access to give it a name. For example, let's call this, I don't know, Next.js tutorial, a description, a complete guide to mastering Next.js. And I'm going to put a price of $19.
And in here, we have the categories. But as you can see, it's loading IDs, right? So how do we fix that? How do we make it load the name of each category? For that, we have to go inside of the categories collection here, add an admin field, use as title, and we have to select one field to use as title.
So let's use name here. So at this point, actually not name, let's use... Yeah, actually we can use name. We can use name, I think. Yes, because it will use the ID.
My apologies. I was thinking in my head, maybe I need to make the name unique and maybe we could do that, like making the name unique. But I think this works just fine. And you can see that now you can select the categories. One thing that I don't like is that we are using the all as the category.
One thing that I was thinking of refactoring later is just adding like we have with this button here, just adding one button here, hard-coded all button, so that then we could exclude all from categories because we still have, you know, a lot of functions here. You can see like this slash all so we could kind of remove that unnecessary checks for all if we hard-coded that button. So maybe I'm going to add a todo, hard-code, all button. So for now, I'm just going to leave it here. So this is in search filters categories inside of your modules home components, a UI component.
I just added a to do here, because I don't think it makes sense that users can select all for a category, right? That will simply be a means for the user to filter by all categories, not for the admin to put a product belonging to an all category. So let's select something else like business and money, for example. Or if you want to make it more complicated, what you can do, because it will give you a better example. Go to localhost 3000.
Choose something like an inner category, like real estate. So I'm going to choose real estate here, simply because it's an inner category. So choose a subcategory because that will make it more interesting to test out. And I'm going to call this product with subcategory and the price of, I don't know, $100. Like this.
And you can also, you know, let's save this. You can also go inside of products here and you can put price, you can add a description, I think, admin description in US dollars like this. And then when you refresh, you can see that it will have a little description here in US dollars and maybe more descriptive price in US dollars. There we go. If you want to, you can do that.
Great. So now we have this product right here. We have the default refund policy and we have a category which is a subcategory. And now what I want to do is I want to load the product based on the category that we add here. So in order to do that, we're now going to go ahead and create the procedures for the products.
So I'm going to go ahead inside of source, modules. And I think we can just copy categories and paste it and call it products. And I think, yeah, We can even leave the types because I think... Basically, let's see. So let's first do server procedures instead of products here, and let's rename these to products router, and getMany will be the only method that we need.
So now let's go inside of TRPC routers app and let's add products products router from modules server procedures a modulus products server procedures here And you can go inside of your products types and you can do products get many output. And in here just select products. In case you're getting any type errors, You should not actually because this is for TypeScript. So now instead of your product server procedures you should go ahead and load products instead. So products like this.
The depth one here will populate something else, not subcategories. The depth one will ensure that image is populated and the category is populated. So this will populate category and it will populate image at the moment, right? Pagination should not be false because we are going to use pagination and we don't need anything for where at the moment and we don't need anything for the sort yet and we don't need to format data at all. We can just return data so we explore if things like these are even true or not.
So no need for this type here. And now what I want to do is I want to go inside of source and I want to go inside of app folder, home, inside of category right here. And now, for starters, I just want to try and load the products here just to confirm it's working. And I think we have an easy way to do that, like products await color from trpcServer.products, get many. I think that's it.
And products is the direct answer here. So we can just do a break method, products, JSON stringify products. So I'm doing this in the category. So make sure that in here, you are in like business and money, something like that. And there we go.
You can see that the category is business and money, and you can see my products here, and you can see the name is real estate. So right now it doesn't matter which category I'm in, it's going to load regardless of the category. But you can see some things that we discussed about. So the category is loaded, right? The category exists, but if I go ahead, whoops, inside of my getMany and change the depth to zero and refresh, I think the category now, there we go, the category is now an ID.
So that's important for you to confirm. If you set the depth to zero, category returns an ID. But If you set the depth to zero, category returns an ID. But if you set the depth to one, in that case, you can see that category becomes an actual object that you can extract the name and the slug from, which is going to be important for us so we can properly display this, right? Great!
So that works. Now what I want to do is I want to create a proper component which is going to be able to prefetch this instead of call it directly and then using suspense load it in a client component. So let's go inside of our modules products and let's create a UI and let's create a components folder and inside we can put a product-list.tsx like this. Let's export product list here. And what we're going to use is the RPC from use the RPC from the client.
And then we're going to go ahead and get the data from use suspense query from 10 stack react query and pass in TRPC dot products dot get many query options like this So we now have a guarantee that the data will be loaded at this time. So what I'm going to do for now is just return JSON stringify data null 2. So basically, that's what we want with this component product list, which will be marked as use client, like that. So that's step one. Now what we have to do is we have to go inside of app, home, category, page, where we did this, and modify it.
Now, I'm not sure if I remember how this is done, so I will just revisit the RPC here, server components using your API. Let's re-learn how to do this. So I need the query client. Okay, I'm just going to add query client here from get query client from at the RPC server, and I no longer need the color. And then I need the RPC from the server as well.
So I'm going to grab it here. And then in here, we simply do void query client dot prefetch query, and I pass in TRPC products, get many, and I think I need to pass the query options like this. There we go. And I think that's it. The next step is to add a hydration boundary here.
So instead of this, I'm going to add hydration boundary right here. And I'm going to pass in the state dehydrate from 10 stack react query here. And I have to pass the query client. So let me just pass it. There we go.
And let's add product list here. And I don't need to pass any props whatsoever. So I'm just going to do a cleanup here. So for now, yeah, we don't need any params. I think I can do this, right?
So I don't have to remove this yet. And let me just align these properly. So now we are leveraging this server component, which is going to prefetch my products, get many and product list here is going to already have them loaded thanks to useSuspenseQuery. But we are also going to add export const productListSkeleton here, which can be a very simple div loading for now. So now we can import that as well, product list skeleton, and we can just pass it here.
Suspense from react, fallback, product list skeleton, like this. Let me just move that here. We can collapse these two. And let's try it out. So this should be working on the category here.
So if I go here in localhost in a category, nothing should be different right now, right? It just works very fast. And you should see a small loading for a second. It's very hard to see, but we can add an artificial loading here. So for example, here's five seconds of artificial development time.
So let's refresh. And there we go. You can see how it's loading. One, two, three, four, five. And then it loads.
Great. So our prefetching seems to be working here. No problem. So what I want to do now is I want to load the correct product based on the category, right? So we can then throw the same component in here in subcategory and it should all be working fine.
And it's going to be particularly interesting because technically if you set a category to mental health, you should load that if you click mental health, but also if you click on fitness and health, because in the parent category, you should also load everything that is set for the parent category, but also every single subcategory, right? So that's what we're going to do now. So let's see where we are. We have added the products collection. And we have started this chapter right here.
So let's go ahead and do it. So I'm going to have one product which is real estate. And then let's see, can I duplicate I can? I'm going to duplicate this and this will be product with category. And the category here will be business and money.
And I'm going to click Save. So now I should have two products. One, Let's see, can I decide which columns to see? You can click on category and now you should see category. So one is the main category and one is a subcategory.
So now I have enough to test both this click and this click. All right, so we first have to go back inside of getMany here. And what we have to do is we have to add the category here. So I'm going to add an input here, z.object, and start building my input. Import Z from Zod.
Let's organize this just a little bit nicer, like this. I like to move this like that. And let's invent this. Let's invent this. And I think the whole thing actually needs to be invented there we go so I'm going to add the category the category needs to be a string nullable and optional and now in here we can extract the input So now what I want to do is I want to check if we have the category or not.
So Before I do anything, I'm going to do if input.category, like this. And if we have the category, what we should do is we should get the category slug, and we should add it to the... Because the Y category slug, right? Perhaps we should be descriptive. Maybe category slug is a better decision here, but I'm going to leave it as category for now.
So this is not category ID. Why are we not sending the ID? Because we don't have it when we click here. The only thing we have here is education, right? And I seem to be having some errors here.
That's fine. Okay. I mean, If it's causing you some lag, you can close localhost if it's causing you some repeated errors here. So if we have input.category, what we have to do is we have to fetch this category. So let's go ahead and do const data, categories data will be await context database find collection categories limit one pagination set to false where slug equals input category And now we should get the category.
So let's do category, categories data, docs, first in the array. So that should be the only one here. And now what we should do is we should prepare the where object, right? So in here we will pass where. So The way I want to do that is by preparing a const where to be a type of where from payload.
So I'm going to import it and I can just do import type where. And by default, it's going to be an empty object. And now I'm going to do, if I have the category here, I'm going to do where category.slug equals category.slug, like this. So at this point, you actually might be wondering, why didn't I just put input dot category in here, right? Input dot category.
Well, I think that we are going to need to fetch the category either way, because we are going to need to load all SAP categories. But I also think it's a good way to check if what you're trying to load actually exists. So yeah, technically, we could have done just this. So I can console this out. And just use where category slug.
And I think that at this point, this could work. And I just think that I need to... Yeah, so we are definitely always going to have options inside. So let's first go inside of the page where we prefetch category page here. And in here, we have the params.
And in here, I can do const category await params like this. And then in here, I can pass the category like that. There we go. And then in my product list, I should also be able to access the category. One way we can do that is simply by introducing an interface props category and let's make it an optional string.
This props category and pass in category. And there we go. Now it matches. What we prefetch now matches what we put inside of the useSuspense query. So right now, I think that we should no longer be getting any errors here.
You can see that all is still loading. Let's see the issue here. So this is inside of my navbar, I think. This is what's the link to the library. I'm going to explore this at one point, but right now I feel OK like this.
So try something other than all. So if I click on Design, it looks like I can still get everything. So let's see why that is happening. I think it's happening because I'm not passing the category here. So let me try refreshing now.
Okay, so it's still loading everything. So I think I did something incorrectly here. Let's see what happens in the procedure. Well, what happens is that I never assign the where in my procedure. There we go.
So you can see that now design, all of this have zero items. But since in my products here, I have one with business and money, so a main category, if I click here, there we go. You can see how it loads. Let's see how many it loads. How many items?
Total docs, just one item, which is what we expect, right? So now we can do the same thing for subcategory. So let's see the title of this project, of this product. The title is Product with Category. So it's the one we expect, this one.
How do we load this one, product with subcategory? That's the complex one, right? So the first way to load it is very easy. We can literally copy the entire page here, go inside of subcategory, and just paste the entire page here. But instead of having a category here, we're going to have subcategory.
But we're not going to change this. We can just do subcategory here, like this. So the prop name will stay the same. It's just that we know that we are working with a param called subcategory, and we can pass that here as well. So I think that this should still be okay.
So now when I click here, nothing. But I mean, it doesn't show that product with subcategory. But if I click here and go into real estate, there we go. Product with subcategory. So it works.
And I just need to check on this to make sure that... I think this just happens in the middle of hot reload. Let me just see, going back, going forward. Yeah, I think that happens in the middle of hot reload. But you can now see that we get different results in the main category and in the sub category.
So my goal to wrap up this chapter is to be able to load in the main category both the sub category and the main category. So in order to do that, we have to go back to this. We first have to actually load the categories data here. And then what we have to do is we have to check if we have the category. And then if we have the category, what we would do right now right is use that category.
Actually, I'm going to call this main category or parent category. If we have parent category, we would do parentCategory.slug. Right, so now we are getting into loading this category, and now what we have to do is we have to check if this, what we assume is a parent category, has any subcategories, right? Let's go ahead and do that now. So what I want to do here in the procedures is quickly visit my categories procedures.
Why? Because in here we have this, the formatted data here, because remember, right now the depth does not change the types here. So what I'm going to do is I'm going to copy my formatted data here, because I like it. And I'm gonna go ahead and try that right here. So after I get the categories data, I'm gonna go ahead and get the formatted data based on the categories data here, like this.
And I'm going to import docs category from add payload types, like this. So now I have formatted data and let's also add that one here. So we know that doc will be a type of category, right? We can copy the same thing here. Depth one means this.
So I can add that here just so you are aware of it, right? There we go. So now we have the formatted data here. And now what I'm going to do is I'm going to prepare const subcategories to be an empty array like this. And instead of doing this immediately, I'm going to get it out of this if clause.
And I'm going to do, if we have the parent category, let's do subcategories.push. And simply spread mainCategory, my apologies, parentCategory, subcategories.map, get the individual subcategory, and extract subcategory slug from it like this. And I think I'm having some Type script errors here. Let me just see. I think this is an extra if clause.
Yeah. And there is no semicolon here. So the error is only in this part. So it says that this subcategory is... Just a second.
Yes, my apologies. The parent category should come from the formatted data and the first in the array. So after we format it and define that subcategory is a type of category, then you can see we can freely access this right here. So we just have to ensure that we add depth one. Otherwise, it's going to break our code completely.
There we go. So now what we're gonna do here is where category slug instead of equals is gonna be in. And then what we're gonna do here is an array where we're going to put the input category or what we can do is just parent category.slug and then simply spread the subcategories which are filled with slugs, right? So we can do subcategories slugs, maybe. I think that's more precise.
Whoops. So just subcategory slugs, subcategories slugs. There we go. And I think that now our business money, our parent category should load two items. And I think I can already see it does.
Let me see. Total documents, two. So the first one it loads is product with category. And the second one it loads, let me just find it, is product with subcategory. Great.
So it loaded it because it detected that business and money has a subcategory of real estate. But if I click on the real estate exclusively, then it detects that there are no subcategories here. So it only loads the subcategory values. Perfect, Exactly what we wanted. Amazing, amazing job.
So that's where I'm going to stop here. I think that we do have some more things on the itinerary, which is to refactor some more things. So we're going to check that in a second, but I just want to show you the depth is very important. So try using depth 0 and this should break it. So right now you can see business and money only loads one page.
So it's a good thing that it even works. But if you do depth one, it will populate the subcategories, which means that you will have access to each subcategory slug. That's why it's important to do depth one. If you are unsure, you can always do a console.log of your category's data, like this. So refresh and then go to your terminal.
And there we go. You can see that in here, You, yeah, maybe use JSON stringify instead. No, use something like this. And then in here, you're going to see a nicer way here. Let me try.
There we go. So you can see, this is how it looks right now. I think that's because, huh, that's interesting. Parent. Oh, I think that the reason this, what we're seeing here, is not something we use, right?
I think this is simply because for each subcategory, it also loads the parent that it has, and then it limits the depth to here. So I think that this is OK. Let me just try and scroll down here. And I am interested about this. A query was dehydrated as pending and a project products get many input category.
This is interesting. I will explore what's going on here. But for now, I think that we did a good job, right? So what I wanted to do in regards of refactoring was, instead of source, app folder, app, Home, instead of Page here. Well, there isn't much we can do here, but let's try Layout.
Yeah, we can do Layout, actually. So I'm thinking whether we should, or maybe this is OK. What I was thinking with refactoring was creating, instead of modules, instead of home, creating layout folder. But I'm not so sure that that's the best decision, really. Because this way it allows me to go to the suspense like this.
So I think I'm going to keep it for components and I will allow layouts and pages maybe. We will see. But for now, I think I'm actually satisfied with this. So I am going to end here. And I will, of course, explore these errors that are happening here.
It looks like somehow our favicon.ico is being passed as the category name. And I think the reason that's happening is because of our structure here, slash category slash subcategory. I think that by default, the way favicon is loaded is by going to localhost 3000 slash favicon.ico, and then that confuses it and turns it into this infinite load type of thing. So I will explore why that is happening. But for now, I think we have proven that our code here works.
So let's go ahead and do the last thing we need here. So we load products based on category. We prefetch in React Server components, and we load with suspense in client components. Now, we are going to skip this part at the moment. I still have to decide whether I want to do that or not.
And what we have to do is push to GitHub. So this chapter is called 11 products. So what I'm going to do is I'm going to close this and git checkout branch 11 products. That's right. Then I'm going to do git add and a dot git commit 11 products and then I'm going to git push you origin 11 products and now you will notice that we are on a new branch here, 11 products, so what we have to do is go inside of our GitHub here and open a new branch, I mean open a new pull request, and now let's wait for our reviewer to see the changes.
And there we go. So the walkthrough. This pull request introduces several updates across the codebase. Two-page components have been modified to adjust parameter handling, switching from a category to subcategory in one case, and integrate asynchronous data fetching using a query client, react query suspense, and hydration boundary. As always, we can see each file change by change.
And here we have a sequence diagram, which helps with explaining what is going on. So user navigates to the product page, we execute prefix query, either with a category or the subcategory here. And you can follow the entire order of events happening with a diagram. I think this is super useful whenever we implement something new like this. It's absolutely amazing.
We can provide prefetch data to the page component here. We can see the getMany called with provided parameter once it actually loads. It's absolutely amazing. So all the way to the database where it returns the product data. It would be nice if it also understood the query client cache, maybe.
So that would further establish what's actually going on here. It also noticed the related PR, which is true. And it added some comments. It's mostly comments ensuring that we have the parent category and ensuring that we throw a state that we handle a case where the category is not found, right? We have to decide what to do with it.
But at the moment I am satisfied with this. Let's go ahead and merge this pull request. I'm not going to delete this branch. I will simply confirm that I have it here. There we go.
11 products. And now I'm simply going to go back to the branch I was previously on. Whoops. So, main or master. And git pull origin main or master should bring you up to date.
And you can always do git status to confirm that everything is fine. And I always like to check the branching to confirm I branched out with products here. And then we merge those changes back. There we go. That's the chapter finished.
We pushed to GitHub. Amazing job and see you in the next chapter.