In this chapter, we're going to continue working on the workflows entity, focusing on pagination and search. Before we go into that, I want to resolve one question from the previous chapter's pull request review, and that was, can we safely use find unique with composite where clauses because CodeRabbit and many other AI models have warned me against using this to the point where I wasn't even sure myself if this is something I can teach you and I'm happy to say that we can use this safely. The thing is this is a very new thing as you can see from version 5.0.0 the generated type unique input on where exposes all fields on the model not just unique fields. So you can see that previously that was only available under the extended where unique preview flag. So here's the rule You must specify at least one unique field in your WHERE statement outside of Boolean operators and then you can specify any number of additional unique and non-unique fields.
You can use this to add filters to any operation that returns a single record. For example, you can use this feature to do permission checks, which is exactly what we are doing. We are adding alongside one unique field a user ID field permission check. Can we fetch this? Are we allowed to fetch this?
So the reason all AI models are throwing this error is because this is a relatively new thing. So yes, I'm happy to say that we can safely do that. And now let's focus on adding search and pagination to our workflows. So what I want to start with is creating a config file folder actually. So instead of source let's create config and inside of here constants.ts.
Let's export const pagination. Default page will be 1. Default... Well let me actually show you all the options. Default page size will be five, maximum page size will be a hundred and a minimum page size will be one.
Great, once we have that, let's go inside of routers.ts, inside of source features workflows, server folder routers.ts. So in here where we have the create procedure, remove procedure, update name, get one, and finally get many. So let's start by adding dot input here for the get many procedure and let's open z dot object and then inside of here let's start by adding the page. So what page are we on of this get many procedure? That's going to be a type of number with a default value using our pagination.
So pagination, make sure you import it from config constants. Let me show you how it looks like this. And then you can use the default page which is set to 1. Like that. Then let's add page size.
Page size is going to be z.number and then it's going to have a minimum and a maximum value. A minimum value of pagination minimum page size and a maximum of maximum page size. And let's also give it a default of default page size basically all the constants that we have here and lastly let's add a search which is Z.string with the default of just an empty string. And then in here besides context we can also export input. Let's go ahead and make this easier for us by destructuring page, page size, and search from the input.
And let's go ahead and turn this query into an asynchronous query because we are no longer going to directly return this. Instead what we're going to do is we're going to get items and total count using await promise all and inside of here open an array and now the first one in here is going to be our existing prisma workflow find many And then the second one will be Prisma workflow count like this. And let's just add where user ID context out user ID. So only count how many total workflows belong to this user. And then in this where we're going to focus on some more specific things.
So right now, both queries look identical. This one uses count and this one uses find many. So then the total count will have the result of this and items will have the result of this. Now let's go ahead and add some pagination properties. Skip will be page minus one times page size.
Take will be page size. Where will be user ID and name. Contains, search. Mode, insensitive. When doing pagination, it's also important to define order by.
So let's do updated at descending like this and I think this will be enough for pagination. Now let's go ahead and actually do some math which will define what is the next page, is there a next page and things like that. So after the promise all is executed, const total pages. We're going to use math.cling total count divided by page size. Has next page will be defined if the current page is less than total pages.
HasPreviousPage will be defined as page is larger than one. Now let's go ahead and add a comment. Map, actually we don't really need to do anything yet. I think for now it's enough that we just return the following. Items is just going to be items.
Page will be page, page size, total count, total pages, has next page, has previous page. And we can also do the same for items in that case. Later, we're going to do something specific with the items that's why I'm a bit confused. Basically we're going to have to map them to their actual nodes and things like that but for now let's just keep it simple. And I think that's enough information for us to have proper pagination here.
What we have to do now is we have to actually pass the props because if you take a look at page.tsx instead of source app dashboard arrest workflows, you will see some errors in the prefetch. If you take a look at the workflows list and go instead of useSuspenseWorkflows, you will start to see errors here. So let's go ahead and close everything so it's easier to understand what we have to do next. So I have actually outlined that here. What we've just did is we updated get many for procedure what we have to do now is add nux for param handling and we have to handle both client-side and server-side param handling so we are going to store the state of our params in the url and there is no better choice for that than the nux package so using the link on the screen you can visit the nux home page and the documentation page and the way it works is it basically allows you to control URL state the same way you would normal use state and that goes for the reactivity part as well which is absolutely amazing.
It's super easy to install all we have to do is npm install NUX and then we have to add a provider. So let's go ahead and run npm install nux here and since we are using Next.js you can even see which versions are supported and I will also show you my exact NUX version so package.json nux 2.7.1 with my next version 15.5.4. And now let's go ahead and go to the adapters. So we are using Next.js app router. So let's go inside of our layout.tsx.
Let me show you where that is located in since I have a small screen here so you can see inside of source app folder the main layout. So source app folder layout where we define the toaster where we import globals where we have the fonts where we have the metadata right where we have html and body define that's the main layout so go here and this is where we add the NUX adapter. So I'm going to add it after trpc react provider NUX adapter and let me indent these two and you can see the full import here for the NUX adapter so I will go here and import NUX adapter from the NUX package forward slash adapters forward slash next forward slash app this is how it looks like in one line and once you've done that I think you are ready yeah you don't have to do anything else here. I suggest that you keep the NUX page open simply so you can look through the documentation as you learn. And now what we're going to do is we're going to actually define all the params that our TRPC router is accepting.
So I'm gonna go inside of source features workflows and in here I'm going to create params.ts. I'm going to import parse as integer and parse as string from nooks forward slash server I'm going to import pagination from config constants The reason I'm importing from server you can also import from here. The reason I'm adding the server prefix is because we are also going to reuse this params object for our server side rendering right for our prefetch. So because of that we need to use this solution because server can be used both in client and server whereas NUX is client so it can only be used in client so since I already know what we are going to be using this for right in page.tsx in the workflows I already know that I'm going to have to load the params here somehow and I can only do that if I think in advance and define the params using NUX server and I don't want to just magically tell you this information you can definitely find that here in the server side usage you can see NUX server right So to parse search params server side you can use a loader function.
That's what we are going to create in a second but you also have to import the search params parse as float, parse as string, parse as integer from the same thing. And you can see some examples here how that's going to look like. So yes, that's why I suggest that you have this open just so you don't blindly accept what I say, try and find exactly what I'm talking about. So let's export const workflows params. The page is going to be parsed as integer with default pagination dot default page with options clear on default set to true.
What does that mean? For example, if we had, let me go ahead and add a comment here, localhost 3000 workflows page and it's on number two. That's pretty normal right? We know what this means but if the default page is number one this is pretty useless because it's exactly the same as this. So for that reason we can tell NUKES that if you hit the default page which is 1 just remove it like this.
Clear on default. That's what that means. Or if you had a search of Antonio, right? And then imagine you cleared the search. Without this option, it would be this.
Which again is dumb because that's the same thing as this. It just looks bad in the URL. So that's what we are doing. That's what clear on default does. And let's go ahead and duplicate this.
The next one is going to be page size with default value default page size and the last one is going to be search which is going to be parse as string and it's going to not use any default, just an empty string with options clear on default true. So basically, params can be anything, right? Just because in the URL, we do localhost 3000 page one, we don't really know that this, is this supposed to be an integer? Is this supposed to be treated like a string? We don't know really.
So by using this strict definition, parse as integer, parse as string, we are telling NUX how to do type safety for that specific param. That's why we have different definitions, because page and page size should definitely be treated as numbers whereas search should be treated as string perfect, now that we have that we have to create a hook called useWorkflowParams inside of workflows, let's go inside hooks use whoops hooks use workflows-params.ts and it's going to be pretty easy now so use query states from nux We don't have to use forward slash server because this is obviously a hook meaning it's going to be used client-side. But let's import workflows params from our shared params and very simply export const use workflows params return use query states and pass in workflows params. So what will this do now? Now that you have that defined, go inside of your workflows.tsx instead of components here and then go instead of useSuspenseWorkflows and in here what you can now do is you can extract the params from use workflows params like this from its neighbor.
And then in here, you can pass the params and you can see how they are immediately type safe because if you hover over params it accepts page, page size and search and our query options accept page, page size, oh sorry this is what it returns. I'm not sure if I can see what it accepts here. I think I can. Input, page which is a type of number or undefined, page size, same, and search which is a string or undefined. So we successfully mapped the TRPC input with use workflow params.
And the reason I did this through NUX is because now it will be super easy to manipulate with this. Basically if you have any kind of URL state please use NUX. It will be such a lifesaver for you. And the best part of it honestly is reactivity, right? So you could add set params here and then if you wanted to, for example, we are going to use this for pagination.
The way we are going to trigger next page is very simply going to be by changing the page param to number two or to number three and that's automatically going to refetch our trpc.getMany endpoint. So we are never going to directly refetch it. We're just going to change the param. But in this case we just want to get the value of them so we pass them here. And that is all we actually need right now.
Instead of this query options for use create workflow, we can just pass an empty object because we don't really care for... We are not invalidating for a specific state, we are invalidating for all of them so we can just pass an object here to get rid of that error and now that we have used workflow params we have to handle the same thing for our page.tsx which is still throwing an error here because it's also expecting some params right so the way we're going to solve that is by creating the loader so instead of server here folder let's create params dot DS so okay it's named the same params- okay let's call it params-loader.ts so params-loader.ts import createLoader from nux forward slash server import workflows params from dot dot slash params export const workflows params loader let me fix the typo, workflows create loader and pass in workflows params now that we have the params loader here Let's go inside of our React server component where we prefetch the workflows. So inside of dashboard arrest workflows page dot dsx. And now we have to pass the props here. So the first thing we have to do is create a type props for this page search params which is a reserved type for each page file in Next.js and it is a promise and the type inside is going to be a type of search params, which you can import from NUX server.
So let me show you search params from NUX server and you can specifically just import it as a type. So search params is a promise exactly the same way when you're going sort of like individual dynamic workflow id page params is a promise. So this is Params is a reserved type for each page.tsx, the same way search params is for queries. So in here we are using this generic search params type. Usually, you would very clearly define search, which is string or undefined, page size, which is integer or undefined, blah, blah.
And you would have to be super specific and just match that one by one with your TRPC router. It's just a mess, right? But thanks to this we can make it better. Thanks to NUX it's much easier. So just get the props here extract search params and Then in here const params are await workflows params.
Oops loader from features workflows server params loader right here and pass in search params from this server component and this will then transform it into our expected type. But keep in mind that this will not fail if it doesn't match the value. I think it even says that here, this async overload makes it easier to use against the search params. I think somewhere there is a warning that it won't fail. You shouldn't basically use this as a validator.
That's what I'm trying to say if that was your plan. But I think that now it should work just fine. If I refresh here we should just see as usual, right? But if I go ahead inside of my config.ts file, instead of my apologies my constants.ts file, and if I change the default page size to one, meaning only one result from the first page you can see that I only get one item inside page size one total count four so I have four workflows in total and I have four pages in total because for each page, I have made it so that it can only be one record. So let me change this back to five.
And now you can see there is one page only because I only have four of them. And for each page, we're going to display five of them, which means that in page one, all of them were created. Amazing, pagination is working just fine. And if you go ahead, maybe you can do this. So if you want to constants change this again to one and then go ahead and change your URL to the following page 2.
So localhost 3000 workflows page 2. I'm gonna go ahead and paste that and you should just see that you are now on page 2 here. Right? Whereas if I go manually to page 3 you will see you are on page 3. If I go back to page 1, the URL should say page 1.
There we go. And also notice how this time, I know it's very small, you can't see, unfortunately. I don't know how to zoom in the URL part, but it still says page one. But when we actually use the hook, use search params, and if I switched to page one using that, it would remove the prop altogether just using the default version. All right, so once you've confirmed that works, we are ready to implement the search.
So let's go ahead and do that. So let's start by going inside of source components entity components and let's start developing the entity search component. So after entity container let's go ahead and define interface entity search props accepting the value on change with the value prop and an optional placeholder then let's go ahead and let's export const entity search whoops like this and let's go ahead and assign all the necessary props inside of here value on change placeholder and set it to search by default if it's not passed and in here return a div with a class name relative ml auto then add a search icon which you can import from lucid react from Lucid React. Give it a class name of size 3.5, absolute, left three, top one and a half, minus translate, minus dash Y, one and a half, text muted foreground. This will position the search icon in the beginning of the input which we are going to add now.
Input should be imported. So, dot slash UI input, the same way we added the button. And in order to make space for the search icon, give it a limit on the width. So let me just move this up to 100 pixels. BG background shadow none border border and PL of 8.
So we leave some space on the left side of the input so the search icon can render and now besides the class name let's go ahead and pass some more attributes here so let's pass in the placeholder prop to be placeholder and then let's add value and then let's add on change on change event target value like this so now that we have entity search let's go ahead and go into our features workflows, components workflows.tsx and let's export const workflows search like this and let's return entity search from our entity components so so far we've imported entity container, entity header and now entity search from here let's pass in the value to be an empty string on change to be an empty arrow function and placeholder search workflows and placeholder search workflows now that you have workflows search you can find workflows container and simply render workflows search here as a self-closing tag. And now you will immediately see it right here. But now let's go ahead and make it work. So I'm gonna go ahead now and grab our params const params set params from use workflows params. And now I can set the value to be specifically search value for example.
But before I do that, I actually want to develop my own function for changing this value using debounce so it doesn't spam the server. So I'm going to go inside of hooks and I will create a new file called use entity search dot TSX so first things first let's import use effect and use useState. Then let's import pagination from config.constants. Then let's define the interface useEntitySearchProps. T extends search which is a type of string and page which is a type of number.
So params is a type of generic set params uses the generic and debounce is an optional config how much to debounce for. Now that we have that let's export function use entity search and let's go ahead and pass the generic here t extends search which is a type of string page which is a type of number. Let's go ahead and extract params, set params and debounce in milliseconds to be 500. Let's return use entity search props and pass in the T generic here and open the function finally. And this should be debounce milliseconds.
Now instead of useEntitySearch, let's first define the local search using the current params.search value local search and set local search with the default value of params.search. Now let's open use effect we are going to use this to create the debounce effect. First let's see whether we actually have to do it. If local search is empty and if params.search is not empty. So this means if new input has been cleared.
So currently in the URL there is a search like Antonio. But user just changed that inside of this input to be an empty string. It means we can just reset everything. So set params, spread the current params so we preserve if there was any pagination here or something else and simply reset search here and change the page to be pagination.defaultpage because every time you search I think there is kind of a practice to reset the pagination. I'm not sure if you don't want that, you don't have to do it, but I kind of expect that as a user.
And make sure to return here to break the method from going any further because what we do further is we define a timer for debounce. Set timeout. If local search is different than what we currently have in the URL, what's the current search, right? So only for new queries, set params. For new queries, set params, search new local search and reset pagination.
Again, this is what I expect as a user. If I search for something and I am on page 50, I would expect pagination to reset. Like I'm trying to look for specific results again. At least that's the way I think of it. I don't know.
And let's go ahead here and pass debounce in milliseconds. Now that we have this, let's go ahead and return, clear timeout and pass in the timer so we don't have any overflow in case this component unmounts. In the dependency props pass in the local search params set params and debounce in milliseconds and last but not least another use effect here set local search params dot search Params dot search in the dependency array. And finally return search value which is local search and on search change which is set local search. And we are going to reuse this for every single entity search with safe debounce.
So now let's go ahead inside of the workflows search here and let's go ahead and use it use entity search which I've just imported from hooks use entity search passing the params set params and you can now extract search value from here and on search change which will be debounced and you can now pass the search value and you can now pass on search change. Perfect! So let's take a look now. I will go inside of my constants.ts and I will change my pagination default page size back to number five. This way I see a lot of my workflows here.
If you don't have one just click new workflow to generate it. Go back to workflows. Let's find a specific name. Flat curved horse. Let's go ahead and search for horse here.
You can see that it's loading and then it will only return the item whose name is flat curved horse. And if you take a look at my URL, it is localhost 3000 workflows Search equals horse. Exactly as I expected. And if I delete this, you can see that the URL is now reset to this. It didn't do this, which would be bad.
Instead, it noticed that is the default. I can just reset and clear the URL so it looks better. Perfect. And you can see how there is a debones, right? So it doesn't query for everything.
Only when I stop typing for half a second and it returns for this one, no items, right? Amazing. You have now created this very amazing search. And the best part, it is also cached. You can see how the second time I searched for horse, it immediately returned the results because it was already cached for this specific query.
That's the power of combining TRPC and 10-stack query and NUX. You get type safety, you get caching, you get speed, you get maintainable code. That's why I really, really like this stack. This is another reason why I feel more confident using trpc as my data access layer as opposed to built-in server actions. I just feel I know how to use this better.
I feel I know how to make better apps using this method. And now to wrap the chapter up let's also add pagination functionality. So we're going to go inside of source components entity components and just as we've added entity search let's go ahead and add entity pagination starting with the interface and the props page, total pages, on page change which accepts page and optional disabled and now let's go ahead and let's actually define the component with the props like this. Entity pagination which uses all of those props and the type right here. And now inside of here let's go ahead and let's return a div with a class name flex-items-center justify-between gap-x to full-width.
Another div inside with a class name flex-1-text-small-text-muted-foreground. And now let's say what page we are on so page, then the page number of total pages or default to number one so page one of two page two of two then another div here with a class name flex items center justify and space x2 py4 with a class name flex-items-center justify-end space x2 py4 and inside let's use a button. We should already have button imported from dot UI button. The first button will be previous and the second button will be next. The first button will be disabled if page is number one or if we have for some reason disabled pagination overall.
The variant is going to be outline, size will be small, onClick for now will be an empty arrow function and then we can copy these props and paste them here into the next button but with a slightly modified logic if page is equal to total pages That's when the button will be disabled or if total pages is equal to zero or if we have disabled all together. Now let's add the actual on page change which we can use right here so on click here let's call on page change math.maximum 1 page minus 1 So we can't go less than the page number one. And make sure that this is like this. And in here we're going to do the opposite. Map.minimum of total pages and page plus one.
So we can't go more than the total pages number. There we go. And we are going to decide what on page change is from the outside. So now that we have entity pagination finished here let's go ahead inside of features workflows and let's go inside of components, workflows.tsx and just above the container here so we leave the container last, we can develop the pagination, export const workflows pagination const workflows use suspense workflows const params set params from use workflows params return entity pagination make sure to import entity pagination the same way we imported entity search and all the other ones right? And let's go ahead and disable it if workflows is fetching Let's go ahead and add total pages here to be workflows.data.totalpages page to be workflows.data.page onPageChange to get the page and set params using NUX, preserving the existing params and just changing the page.
So if we have a search query and then change to the next page, we should not reset the search query. We should keep it. And now that we have workflows pagination, we can render it here as well as a self-closing tag. There we go. You can see that since I have typed in horse, that's the only existing query.
So page one of one. But if I clear this. I still get page one of one. Okay, not too useful of an example. We can very easily change this by going inside of our constants and change the default page size to number one and refresh.
Immediately you can see page one of five with the next button active. So if I click next, there we go, page 2 of 5 and I am now on page 2 and notice my URL. This is my URL and yours should be too. Page two. And if I click previous to go back to page one, you can see that now the URL is properly reset to default.
Right? This is my URL now. It didn't go to page 1 because it doesn't make sense. Page 1 is the same thing as this. That's what clear on default does in the search params NUX configuration.
Amazing! And if I search for horse for example, in that case, let's see what happens. So horse is here and I have one of five, but I can still click on next. Oh and that just makes an empty page. Okay.
Ah, I see. I see what we did wrong. We have a bug. Inside of routers.ts, get many. When I do a total count, I don't take in consideration the name, the search.
There we go. So make sure you add this to count. And now you can see now it's a bug, page 2 of 1. So let's try this again. And let's click horse.
When you search horse now it will give you page 1 of 1. But if you remove it will be page 1 of 5. And you can also see that this is cached too. Right? So only the next ones are going to be slower, but the previous ones are cached, so they're super fast.
Amazing, the pagination is working, the search is working, let's search for pencil. There we go, page one of one, previous and the next disabled, the URL is working. Amazing amazing job, you just developed a kick-ass pagination and search with debounce available to reuse for all of our other entities. Let's bring this back to five. So we have a more realistic example for future use cases.
And now let's merge this. So I have 13 unstaged files. You might have 14 as always in case Mprox leaves some logs. I'm just telling you that in case you are comparing to yours and are wondering where did you get the extra file, right? Perfect, So let's go ahead and see if that's all we wanted to do.
That's all we wanted to do. 12 workflows pagination. So I'm going to go ahead and start a new branch create new branch 12 Workflows pagination. I'm then going to stage all of my changes and add a commit 12 workflows pagination. And I'm going to commit and I'm going to publish the branch.
And as always once the branch has been published I'm going to go ahead and open a pull request. This was also a pretty large one, so it will definitely be a good idea to have another pair of eyes take a look at it. And here we have this summary by CodeRabbit. New features. Workflows list now includes a debounced searched input.
We added pagination with previous and next controls as well as page indicators. Page, page size, and search are synchronized to the URL for easy sharing and persistence. Yes, when you add state to the URL you can also very easily share that state if you ever modify this to have an organization level. Improvements. Workflow data prefetching now respects current filters for more relevant initial results.
Exactly this way we are prefetching exactly what's in the current state of the URL. So if user refreshes on that page we are going to prefetch that exact state. As always file by file walkthrough here and here we have the sequence diagrams so not need to you know look at this too much simply because we've already went over this when user navigates to workflows What's different now is that before we call prefetch we also create the loader with the current search params. So this is a specific example if we have search and if we have page we're going to create the loader using NUX and NUX adapter and then we're going to parse those fields into page, page size, and search which are compatible with prefetch workflows params which are inferred to trpca workflows get many input And then that will properly prefetch from the database using the proper count, using the proper search, using the proper page. Perfect.
And down here we have an example of how debouncing is working, but I think we are all already aware of how that works. If for whatever reason you are curious, feel free to now pause the screen and take a look at how this is executed here. We do have a few comments here. So first one is invalid NUX dependency version. I think this is not true.
I'm pretty sure that this is a completely valid version of NUX that we have here. In here it suggests adding area, area label for the placeholder to improve accessibility which we could definitely add, yes. So area label for the placeholder makes sense here. In here we forgot to add the bounds validation for the page parameter. So we add default, but we never limit the minimum value.
So we could definitely add that. So users can't query by negative pages. And in here we have a potential issue and a serious one potential infinite loop because we use params object in the dependency array. So the solution here to could be to restructure to use to depend only on specific fields as you can see params search params page. The problem is we want to preserve the existing params here.
So I will have a look at how we can improve that for the next chapter. Though we tested it and it didn't cause any infinite loops. So we can also say that it works. But yes, this is a good thing to reconsider. Let's go ahead and merge this pull request.
And once we have merged this request, let's go ahead and take a look if everything is as intended. So I'm going to change this to my main branch. And then I'm going to click on the synchronize changes button. And then I'm going to click on the synchronize changes button and then I'm going to click on my source control here. I will open graph and in here you can see that we have properly merged 12 into main.
Amazing! And now let's go ahead and wrap this chapter up. So we have added NUX for RAM handling both client and server side. We added more entity components as well as full UI for search and pagination. And we've wrapped it all up by reviewing our pull request.
Amazing, amazing job and see you in the next chapter.