In this chapter, we're going to implement agents filters. So the reason I didn't put any screenshots here is because the filters were already featured in our chapter 12. So it's gonna be this search bar and the pagination right here. We're going to start by modifying our agents.getMany procedure. We are then going to add a package called Nux.
We're going to implement the UI components for the filters, which will include the search input and pagination. And we are then going to synchronize the react server component filters and client filters because it's very important to do that when we do refetching. Let's start with modifying the agents get many procedure and before we actually do anything well as always confirm you are on your default branch and confirm that you have synchronized your changes here and then head inside of modules agents UI views agents view and in here we have this bug And the bug is because in the get many procedure here, I don't have the meeting count. So what I'm gonna do is I'm just going to copy what I did for the get one, and I will add it in here like this. And as you can see immediately, the error now goes away.
And I think that now, so all that time in previous chapter, I was adding the meeting count in the get one procedure, and I was so confused why I couldn't read it in my table here. So I think that now, once I add it to the getMany procedure, just a hard-coded number 5, I think that we can go inside of the columns, so inside of agents UI components columns. And I think that I can actually now try reading row. My apologies, let's destructure the row here. Row original meeting count.
Can I do that now? Let's refresh. There we go. So number five is rendered here. So if I change this in the get many procedure to number six and refresh it shows number six.
Perfect. So now we can go ahead and add that little tweak here. If row original meeting count is equal to one, show meeting, otherwise show meetings. So it shows the proper, the grammatically correct text. Perfect, so that's the first issue fixed.
We no longer have any errors inside of our agents view. The second thing that we now have to do is go back inside of our get many procedures. And what we have to do is implement the proper filters here. So before we do .query, let's add .input here and let's open up an object so I think we already have Zod imported perfect so now inside of here we can add the filters the first one will be the page which will be a type of number and the default will be one as in the first page. And for the page size here, we're going to add z.number with a minimum of one.
And let's add a maximum of 100. And the default of whoops, let me just go back and the default of 10. And then we're going to add search, which is a string and nullish, nullish, this one, like this. And you can set the entire input to be optional. My apologies.
So you have to go inside of this parentheses and then put optional here. The reason I want to put optional is because we have default values for the page and the page size. So if the user doesn't want to pass any filters, they don't have to pass any filters, right? And also, here's another thing you have to be aware of. And let me just indent this a bit like this.
I just want to format it a bit nicer so it's clearer which part is which. There we go, like this. So protected procedure as an input. Inside of that input, we have an optional object, which has three possible parameters. The page, page size, and search.
Why did I add optional to the entire Z object? Well, for example, if we go inside of our AgentForm component, you might remember that in here we do some invalidation here, right? Specifically, what we do is whenever we create a new agent, we refetch AgentsGetMany by invalidating it. Now, if I were to change this to not be optional you can see that I immediately get an error here because this expects the object inside for the options but since I am invalidating this I don't really know what to pass here except just an empty object. So it kind of looks weird, right?
And not only that, but for example, in my agent's view here, I also have a problem. Even though I just want to use the default options, I now have to just pass an empty object inside. Right, so that's why I prefer to add .optional to the input here. I mean, sorry, to the Z object here. And instead, just make sure that I have some defaults.
For example, the search doesn't need to have a default, but pagination needs to have some defaults. And if I handle the defaults inside, I can safely put the entire object to be optional. And guess what? I don't have to modify the rest of my existing queries here. Right?
I don't have any errors. Great. So Now I don't like magic numbers in my code. So what I'm going to do is I'm going to go inside of a source folder and create constants.ts. And inside of the constants file, I'm just going to export the default page to be number one, default page size to be 10, maximum page size to be 100, and the minimum page size to be one.
So just these four constants here. Let's export them. And the reason I put them in the constants folder file in source is because I will use the exact same defaults and constants for all of my get many procedures. If you want to have different, you know, per module, you can then put it inside of the agents module and use them, but since I know that all of them are going to be the same I will just use them in the source folder. Great so now let's go ahead and replace the page default with default page from the constants right let's import it then The minimum here will be minimum page size from the constant, maximum page size, and default page size.
Make sure to use default page size and not default page here. So you should have all of these imports here. There we go, perfect. So now that we have added that, we can work with those inputs here. Let's destructure the context and the input from here.
And the first thing I want to do is the following. I want to add a where here. And inside of here, since I already know that I will have multiple equals, I will add and from Drizzle ORM. So make sure you import and from drizzle or M and then I'm going to open this and the first one will be an equals for agents user ID to be equal to my Context out user ID or you can use context out session user ID, whichever you prefer. They are the same So that's the first thing that I want to load.
I only want to load agents that this user created, because in the create procedure, we set the userId to the currently logged in user. So that's how I know that this will work automatically. But besides that, we now also have some more filters like the search filter. So for this, we can check if we have the search. And if we do, let's use I like from drizzle ORM.
So make sure you add this import here and Let's go ahead and pass in agents.name, like this. Open backticks. Go ahead and add percentages and render the input.search, like this. And this will be input.search. Whoops.
Like that, otherwise undefined. Perfect. So now that we have that, let's go ahead and now that I'm thinking, I kind of don't like this. I don't like that the input has to be accessed through a question mark here. I have a feeling this might complicate things.
Because even though we've set these things to default, the input is, as you can see, it's possible for it to be undefined. So I'm going to revert my decision. I know I told you to put this to optional, but I would rather we don't do that. So, remove optional from here. And you can see immediately I have errors here.
I would rather we handle those errors than have unsafe access to the variables inside my input. Because what I can do now, for example, is I can extract search page and page size from the input safely, right? Whereas if this is set to optional, you can see this is any, right? Obviously this doesn't work properly. So I don't know, I think I would rather do it like this because now I have the proper information.
I think one possible solution is to set the input like this and then its number are undefined. But then again, you know, I don't like working with values like this because this is technically not correct. Page size will always be a default something. So it's obviously not inferring these things correctly. So that's why let's not use optional.
I regret doing that. So I'm going to remove it now and I'm going to instead do what I said I didn't like but let's do it. So add this Z object to your protected getMany procedure. You can go ahead and destructure these items here, leave everything as it is, and let's fix our errors. So what you can do is you can search for agents.getMany, and you will find all the places you are using it.
So let's start with the first place here. This is the source app dashboard agents page where we prefetch. What we have to do now is add the empty object inside. Let's go ahead and do it again. Agents get many.
The second one is inside of the agent form. So inside of modules agents UI components agent form. When we invalidate the queries, pass an empty object here. Great. And the last one is inside of the agents views.
So source modules, agents, UI, views, agents view. And go ahead and pass that here. There we go. And we just resolve all syntax errors. And we can now safely destructure the search page and page size from the input here.
And then I can do search here and I can do search here. There we go. So it's easier for us to build these queries. Okay. So yeah, you can see me change my mind in real time now.
Great. Now let's add order by here and let's add descending from drizzle ORM. And in here, descend by agents.createdAt. And then combine it with descending agents.id. The reason we need the second one is for pagination.
So let's add a limit to be pageSize here. And let's add an offset here to be open parentheses page minus one multiplied by page size so each following page will offset the last result and now let's calculate the total so const total will be a weight database select count and use the count not from the not from console use it from drizzle or m and execute it So make sure you import count from drizzle or M and add from agents here and where just go ahead and copy the query like this. But in here we will not do any ordering or any limit or offset. So we are interested in the total number of items that we are working with because Only with the total number can we calculate the total pages. So let's use math ceiling total, first one in the array, dot count, or what you can do is just the structure total like this.
So then you can do total dot count, looks nicer this way, and divide it by page size. And then in here, instead of returning the entire data, what you're going to do is you're going to return items under data, total under total.count, and total pages as a separate last item. Great. And now that we have introduced this, we have to go ahead and fix again, all the places that use getMany. So let's search for agents getMany.
In the page, everything seems to be okay because we are not actually working with the data. Agent form, everything okay because we're not actually working with the data. Agents view, and we have some problems. The one we can fix immediately here is data.length. So now what we have to do is data.items.length.
And in here, data.items as well. There we go. So that's how we fixed that issue. Now let's refresh and let's see. There we go.
I can only see one now. And let's go ahead and let's try creating some more elements here. For example, let's create five of them just so we can try out our filters. OK, so I have five agents, and I will go inside of my procedures, get many. And I will change the default page size here to be, for example, two.
So when I refresh now, you will see these two right here. And even though I entered the exact same name for them, which is a coincidence, you can see by instructions that they are different agents here. So we can now bring this back to 10 and just refresh. There we go. And also, You can now try and changing inside of useSuspenseQuery here.
If you want page size, for example, you can do it here. But this is also where you will probably encounter this issue. And it says unauthorized. Why does it say unauthorized? It seems like a pretty cryptic error for a very simple thing we did.
We just used the options that were given to us. Well, the reason why is because you're using useSuspenseQuery. That means that it's expecting the data to be populated from somewhere. More specifically, data populated from dashboard agent's page prefetch. The difference is, In the server component, you prefetch without any explicit page size, but in here, you are fetching with the page size.
So what happens is that this useSuspense query notices, hey, I have different query options. That means I cannot guarantee that I will have the same result as the cache that was given to me. So I will fall back to useQuery. The problem is, one thing that doesn't get transferred when it does its internal fallback to use query is the headers, which means that it loses the authentication. So that's one gotcha, as we would say, right?
That's one potential foot gun with using this and not being careful. So the error is very cryptic. It just says unauthorized. That's because we are authorized when we prefetch, but in here we lose that chain of events because we have different query options. So in order to not throw an error, but to still attempt to load the data, what we do is we switch to use query, and Then we load with our new query options.
And somewhere in between, we lose those headers and we fail to call that procedure because that procedure is protected. And then we get into this weird state where the data actually loads, but we still get this error, right? So it's kind of weird, but the fix is very easy. You just have to make sure that what you prefetch is exactly the initial state of what you expect in the useSuspense query. You can later of course change this to be page size 10, right?
You can modify this later, but the initial load needs to be exact. Once you do this and refresh, no more errors, right? But if one of these doesn't match you will start getting that unauthorized error because the queries don't match so now go ahead and clean this entirely make it an empty object and make it an empty object here. Perfect. What we're going to do now is we're going to add NUX.
So let's go ahead and do npm install nux –legacy-peer-deps and I will show you my NUX version just in case. Oops, package.json, NUX. So 2.4.3. The major version here is quite important because NUX1, for example, is a completely different usage than NUX2. So in case your NUX behaves very differently than mine, feel free to install NUX 2.4.3 to get the exact same experience as I do.
Now let's go ahead to our root layout application. So source app folder layout and in here import NuxAdapter from NuxAdapters.next. And go ahead and wrap the entire app inside of the Nux adapter. There we go. And now you will see what Nux basically is.
So what I like to do is I like to create a use filters hook inside of my respective module. So let's go inside of agents here and let's create hooks. Hooks and inside of here go ahead and add use filters or let's do use agent agents filters dot DS like that and inside of here what you're going to do is import parse as integer, parse as string, and use query states from NUX. Export const use agent filters, agents filters, and return use query states here, and passing the search to be parse as string, dot with default empty string, be parseAsString.withDefault empty string and with options clear on default true. Copy and paste this and set the second one to be the page and this will be parse as integer like this oh and this is an object not an array sorry there we go and with default it's going to be a default page from the constants, like this.
And clear on default will be true. So one thing that we are not going to add here is the page size. Why? Well, because NUX is basically a way to synchronize your search params, for example, search hello with your use state. Let's call it like that.
So we are basically going to use Nux as a way to synchronize the URL state with our React component, right? It's going to be a two-way binding where they will control each other in sync. But what I don't want is for the user to be able to append, you know, page size 1 million and break our app. That's why I won't allow the user to change page size from the URL. Great.
So that is use agents filters here. And now that we have that, we have to go ahead and actually, well, use them. So this is what we're going to do. We're going to go inside of source, inside of modules, agents. And let's go inside of UI components, agents list header right here.
And in here, I'm going to get the filters and set filters from use agents filters. So you can see that it behaves exactly like useState. So you get a very familiar API and a very powerful feature. So now let's go ahead and create the search filter. So I'm going to go ahead and go for the search filter.
Yeah. Let's do it inside of the component here. Let's call it agents search filter dot dsx. Import the search icon from Lucid React. Import the input component from components UI input, and import use agents filters from hooks.
Export const search filter, extract the filters and set filters from use agents filters, return a div with a class name of relative, render the input component here, give it a placeholder of filter by name, Give it a class name of Height9, background color of white, width of 200 pixels, and PL of 7. Give it a value of filters.search. And on change, grab the event and call setFilters just as you would setState and set the search value to eventTargetValue. And then add a search icon from Lucid React. Make sure you add this import here.
And add a class name, size4, absolute, left 2, top 1.5, minus translate on the Y axis, 1.5, and text muted foreground. Great. Now let's go back inside of our agents list header here and let's go outside of this div here and add a div and add a div with a class name flex items center gap x2 and padding of 1 and render the search filter component And let's actually call it the Agents Search Filter, since the name is already Agents Search Filter. And render it here. And now in here, you should see a text filter by name.
And I want to bring your attention to the URL. So this is my URL at the moment. Whoops, let's go here. This is my current URL. HTTP localhost 3000 agents.
When I type test, you can see that it immediately changes to this. It depends the test there. And if I change this to test one, two, three, So if I change my URL this time and press Enter, you will see that this is a two-way binding, meaning that whatever I change in the URL will appear here and whatever I change here will appear in the URL. And you're probably wondering, what does the with default and clear on default do? Well, this is simply to improve user experience.
So the default search is an empty string. And in here, we are telling it, if you get an empty string, simply remove this from the URL. So we don't want a scenario where this is the URL. It just looks weird. Why not just remove it?
So that's exactly what will happen if I clear this. You can see that now my URL is back to the original. So that's what the with default and clear on default options do. Perfect! So now that we have that, what we can do here is go back inside of the agents list header And now let's do this.
Const isAnyFilterModified and simply do double exclamation points and look for search. Then open a new function onClearFilters and call setFilters here, search to be an empty string and page to be one. Or we can use default page from the constants, just so we avoid any magic numbers. There we go. And now, after agent's search filter, open is any filter modified here and render a button component with an X circle icon from Lucid React and a clear text.
Give the icon a size 4. Actually, no, you don't have to. It automatically gets size 4 if it's inside of a button. Make sure you have imported the button and X circle icon here. And instead, give the button a variant of outline size of small and on click on clear filters so now if you type something you can clear it with this great What we have to do now is we have to connect this with our actual agent's view here and pass the search as an option here.
And we can do that quite easily by adding our filters hook. So, filters, use agents filters, like this. And simply go ahead and spread the filters. We don't have to even individually add search and then filters.search. We can simply spread all of them because we know that both search and page exist in the options.
So now by default, it will use an empty string for the search and number one for the page. But if you try this right now, so go ahead and clear this, and if you refresh, you will get an error, right? Unauthorized. But if you try searching, it will most likely be able to query. The only problem is that when you refresh, when this is like the initial state, you will get this unauthorized error.
That's because I explained previously, you need to match your initial load with your server component, right? So now we have to do the same thing here. But how do we do that when we cannot access a hook here? Well, they thought of that too. What we're going to do now is we're going to go back inside of our modules, agents, and let's go ahead and add a new file called params.ts, like this.
Let's close all of this. So just add params.ts here. And I want you to go instead of hooks, use agent filters. And I want you to copy this, keep both of them open actually. Go back and set up the params, paste it here, but add a forward slash server import here.
Go ahead and import the default page from constants here and export const filters, search params page and simply copy everything that you had here. So you can just copy these two. Like this. There we go. So search parts as a string and this parts as integer and no need for use query states actually from the NUX server only these two there we go and then export const load search params create loader from NUX server and pass the filters search params So now we have the equivalent for a server component.
Now you probably have a question on your mind. Could we find a way to reuse this? Like can we copy this somewhere and then use it here and use it here in the same time. You probably can. The problem is you would have to import everything from Nuke's server and I just haven't found proof that you can do that safely.
If you research yourself, go to the NUX documentation page. Maybe you will quite easily find an example where they show you that you can use NUX server in client components. But I just don't want to teach you anything incorrectly. So that's why I'm repeating myself twice, once for the client and once for the server, just to keep it safe. So yeah, make sure to just copy these properly so there isn't any mismatch, because mismatch will continue to cause the unauthorized error, right?
Great. So now that you have the params here, go back to your, not the view, but go inside of the app dashboard agents page here. Create an interface here. And what you have to do is add search params, a type of promise, and search params from NUX, and you can import it as a type. Let's move it here.
And then go ahead and destructure the props here. Grab the search params and then simply get the params from await loadSearchParams from modules.agents.params and pass in searchParams. And then you finally have your params here, or you can call them filters, for example. So you use the same keyword as on the client and simply spread the filters. And Now you have officially synchronized the server component and the client component.
You can see no more errors. It now simply has the exact same state on the client and the frontend. No matter what I do, I can properly query this. No matter how I refresh, no matter if I manually add search and do something and press enter no errors because the server component and the client component are synchronized by their initial query which is exactly what we wanted to achieve perfect And just one important thing to know, loadSearchParams does not validate the search params, just in case you were wondering, right? Now, let's go ahead and let's wrap this chapter up by adding pagination because we just added search and now we also have to add pagination.
So let's go instead of the agents view here and instead of the use agents filters, let's also extract the set filters here. And then just below the data table here, let's add data pagination here. It doesn't exist yet. We're going to create it. And let's prepare the props.
The page prop will be filters.page. The total pages will be agents.data.whoops.data.totalPages. And onPageChange will be a function which accepts the page and simply calls set filters with the new page. Now let's go ahead and implement the data pagination component here. So I'm going to call this agents data pageant.
Actually, we can call it data pagination.tsx, data-pagination here, like that. And let's first of all define our props. Page is a number, total pages is a number, onPageChange is a function which will set filters with the new page here. And let's also import button from at slash components UI button. Let's export cons data pagination here.
And let's destructure the props page, total pages and on page change here and let's return a div with a class name flex-1, text-small and text-muted-foreground, and simply write page, then the number of the page, of total pages. But in case total pages is zero, that will technically mean for the user page one. Internally on our server, if there is no data at all, then the total pages are zero. There are no pages to be made. But for the user, it's weird to show them page 0.
That's still technically page 1 for them. So that's why if we get 0, we're just going to display the number 1 for the user. And below this, Let's add a new div with flex-items-center, justify-end, and gap, whoops, and space-x2 and py of four. In here, let's add a button with the text previous and another button with the text next. And now what we have to do is just assign some props to them.
Let's set the disabled here to be page one for the previous and for this one if page is equal to total pages, or if total pages is equal to zero, like this. The next one will be the variant, which is the same for both of them, outline, size, small, and then we're going to have the onClick. The first one, or the previous one, will have onPageChange, math, max, one, or page minus one. So we are basically ensuring that we cannot go into negative page requests. Right?
The lowest one we can go to is Page1. And for this one on page change, MathMin. TotalPages, otherwise PagePlus1. This will basically ensure that we cannot go above the total pages. I mean, we cannot make a request above total pages.
And that's it. That's our pagination. And now let's go back to the agent's view and let's import this from components data pagination. And there we go. You can now see page one of one here and the previous and next buttons which are disabled.
In order to see the pagination in action, the best way is to go inside of the constants and simply change the default page size to be one or two. And then you will see that you have the next page active. So when you click next, you will see the next set of results. And take a look at your URL. It should be showing page 2.
So if I manually change this to page 3, it should load the page 3. And I can go back previously. And you can see when I do a refresh, it stays on page two. And there are no errors because we properly synchronized the client and the React server component. So you can now bring this back to 10, you know, just show the normal amount of data.
Just you can always reduce it if you want to test your pagination. Amazing, amazing job. So you created a lot of components now, which we are going to easily reuse for our meetings. So we won't have to build all of this all over again. Specifically, some components we are going to copy, but some we're going to actually reuse.
That's why some of them in here have generic names, like data pagination and data table. So we are later going to move these two into global components because they will be reusable. But these ones aren't exactly reusable, but we will be able to copy their code and then just build faster, right? If you want to, you can make them reusable, but sometimes I think too abstraction is not too good of a thing to do. Great.
So now that we've had this done, let's just review. We have modified agents get many procedure, we added nukes, we added UI for filters including the search input and pagination, and we synchronize the React Server component filters and the client filters. And now let's merge this. So I'm going to go ahead and change to a new branch here, 13 agents filters. Once I confirm I'm on a new branch, I'm going to stage all of these changes.
13 agents filters as my commit message. I will press commit and I will publish the branch. There we go. And let's go to GitHub. Let's create a pull request here.
And let's see what CodeRabbit has to say. And here we have our code summary. Let's go through the walkthrough. This update introduces pagination and search filtering for agents, leveraging the new Nuxt dependency for query state management. It adds constants for pagination, new hooks and components for filters in pagination UI, and modifies the agent's query to support paginated and filtered results.
And the root layout now wraps providers with a new NUX adapter. In here we have a detailed file by file change summary. In here, we have a sequence diagram explaining our new filters here, combined with the actual Nooks use agents filters. So again, if something is not clear regarding this chapter, these kinds of diagrams come in very handy, as you can see exactly what's going on. So the user loads the page with search params.
The agent's view components gets the filter state, search and page using the use agent's filters here. We fetch the agents with those filters. The agents server procedure then returns that in form of items total and total pages and then we render the page. By clicking on the next or previous we update the page in query params and that triggers a data refetch with the new filters. And no comments!
That means we did a very, very good job so we can safely merge this pull request. Perfect. Once this pull request has been merged, let's go back inside of our IDE. Let's go inside of our default branch, in my case that is main. And let's go ahead and hit synchronize changes right here.
And then you can go inside of your source control, open the graph and confirm that you just merged the agents filters. There we go. Perfect. So that marks the end of this chapter. Amazing, amazing job and see you in the next one.