Building Filters and Pagination in React [with examples]
At this point, we have a page displayed in the `/listings/location?` route that surfaces up to eight different listing cards for listings that exist in certain locations. In this lesson, we'll create the capability for a user to filter and paginate the information presented to them in the listings page.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two with a single-time purchase.
[00:00 - 00:17] In the last lesson, we were able to build the page displayed in the listings route, where we show up to eight different listing cards for different locations that's derived from the URL parameter in the route. With that said, there's still a few things left for us to do in this page.
[00:18 - 00:41] One thing we've mentioned is that we're interested in having the capability to provide the user the option to filter listings from one of two ways, whether it's to see listings sorted from the lowest price to the highest price, or to see listings sorted from the highest price to the lowest price. In addition, we also want to give the user the option to move from page to page .
[00:42 - 00:58] We only show a maximum of eight listings per page, and if more are to exist for a certain location, we'll like the user to be able to move to that next page. If there's only to be a single page of listings, we'll avoid showing this pag ination section completely.
[00:59 - 01:15] Both the filter section and the pagination section within this page are to be child components of their own. The markup for these components will be small, but depending on the user action conducted, a re-query for new data will be made to show a new set of listings.
[01:16 - 01:31] Some other things we'll want to take care of is appropriate to UI for when the query is loading or when the query might have completely error it out. When the query is loading, we could use the shared page skeleton component we 've built for some of the other pages.
[01:32 - 01:49] However, for this particular page, we'll go the extra step and look to create a custom loading page that sort of tells the user what the perceived UI would be. So the loading state would essentially be a grid of eight cards that resemble that they're in the loading state.
[01:50 - 02:01] Query similar to how we've done it in the home page, except we'll be showing eight cards here instead of four. And this will be a listings skeleton component that we'll create.
[02:02 - 02:32] And lastly, if an error was ever to occur when the query in this page is made, a suitable example of an error actually happening would be if a country can't be found from the geocoder, which in our case we've actually made our query to error out completely on the server side. If that is to happen, we'll simply show the listings skeleton component and we 'll display an error banner up top that says we couldn't find anything matching your search or we've encountered an error.
[02:33 - 02:53] If you're searching for a unique location, try searching again with more common keywords. So in this case, we tell the user to conduct an action of trying to search for a more unique location, just in case they've tried to search for a location and a country wasn't found where it should have been found.
[02:54 - 03:03] We'll take this step by step. The first few things we'll tackle are the filtering and pagination capabilities and we'll begin with the filtering capability.
[03:04 - 03:17] The query at this moment accepts a filter variable that dictates how the listings are going to be sorted. And we've simply provided a value of price low to high from the listings filter enum.
[03:18 - 03:36] If we want to give the user the option to control how the listings are going to be shown and sorted, we'll need to give them the ability to control the value we pass in this filter variable for our query. So because of this, we'll keep the value here as part of component state.
[03:37 - 03:51] So the first thing we'll do is import the use state hook from react. In our listings component function, we'll use the use state hook to initialize a state variable we'll call filter.
[03:52 - 04:10] We'll also destruct a function that can be used to update the filter state variable, which we'll call set filter. And the initial value of the filter state variable will be the price low to high value in our listings filter enum.
[04:11 - 04:39] Notice that when we provide the initial value of the state variable from the value within an enum, TypeScript knows that the filter state variable is of the enum type, which is helpful for when we intend to set its value later, this will help ensure we set the new value that's also part of the enum. We'll now update the filter variable in our query to take the filter state variable as its value.
[04:40 - 04:51] And here we're using the ES6 property shorthand syntax. At this moment, our listings page should behave the same as it has before.
[04:52 - 05:12] Now we'll look to create a component called listings filters that will accept the filter state variable and the set filter function. And we'll have the markup for the select dropdown, which will display the initial values, whatever the filter state variable is, and when a change is made, it would call the set filter function.
[05:13 - 05:29] We'll first create the component before we import it and use it within listings . So we'll create a listings filters folder and a components folder within the listings directory that is to have an index file.
[05:30 - 05:53] And we'll create an index file in the components folder for where we can re- export the soon-to-be created listings filters components. The markup for the listings filters components will be pretty small and will mostly consist of the select dropdown that we'll use from end design.
[05:54 - 06:25] End designs select components provide different variations of how it can be used, but we're going to use the more basic variation, which is simply using the select component, which accepts a default value or in our case we'll use a value prop that determines what the current selected value is. And within the select dropdown, we'll declare the option sub-components that convey each potential option and we'll have the value for each option attached to them.
[06:26 - 06:41] And finally there'll be an on change prop function in the select component that will trigger a callback function that we'll use to update the state value. So let's see this in action.
[06:42 - 07:04] In the listings filters file we'll import the react library and the select component from end design. We'll also import the listings filter enum from the global types file with which we'll use to dictate the value of each option in the select dropdown.
[07:05 - 07:37] We'll declare the shape of props that this listings filters component is expected to accept. We've mentioned that this listings filter component will accept the actual filter state variable from the parent, which is of type listings filter and a set filter function which can be used to update the filter variable and will take a new filter value argument of type listings filter and will return void.
[07:38 - 08:06] And we'll destruct the option sub-component from end designs select components. Now we'll create the listings filters component function, specify the props it is to accept, and for its mock up we'll return a div element that at first has some text that just says filter by.
[08:07 - 08:27] This will hopefully convey to the user that they can filter the listings by whatever the options are going to be stated in the select dropdown. And then we'll have the select dropdown.
[08:28 - 08:44] The value will be whatever the filter state value is. When a change is made it will trigger the set filter function and pass along the new value.
[08:45 - 09:01] And we'll have two options, one to say price low to high and the other to say price high to low. And we'll provide the appropriate enum values for each.
[09:02 - 09:37] So the price low to high enum value is for the one with the appropriate text price low to high and the same for the price high to low version or option. And now in the listings page component we'll import the listings filters component from the adjacent components folder and we'll look to render it in the listings component itself.
[09:38 - 09:51] As to where we'll render it, we'll render it when listings data is available and listings are actually present. There's no point in rendering the filter options when there's no listings at all.
[09:52 - 10:16] So with that said in the listings section element instead of just rendering the list we'll render a div element that contains the list and we'll have the listings filters components be shown and we'll pass along the values for the filter and set filter props that it is to accept. Cool.
[10:17 - 10:28] So let's see how our page now behaves. If we look at our listings page we'll now see a select drop down be shown to us with two options.
[10:29 - 10:38] One that says price low to high and the other that says price high to low. By default we can see the filter value is price low to high.
[10:39 - 10:57] And at this moment I'm looking at the listings we have for the Los Angeles area . If we picked the other option of high to low after a brief second a new set of listings is shown to us and now are sorted from a price of high to low.
[10:58 - 11:09] And if we toggled back and forth we can switch from seeing the different sorting styles. So what's really happening here?
[11:10 - 11:29] I think we've seen behavior similar to this when we built the user page with some pagination but this is very important so we're going to break it down step by step. When we first load the page the filter value is set from price low to high which is why we see listings sorted from price low to high.
[11:30 - 11:46] This is what the server returns from our initial query. At the same time the select drop down shown to the user shows the initial state value of the filter which dictates that it's from price low to high.
[11:47 - 12:03] When the user selects the other option in the drop down the set filter function in the listings component is run and the new filter values passed in. The set filter function changes the value of the filter state variable.
[12:04 - 12:25] In React when state changes components often re-render to show the new change. Now the use query hook does make a query when the component first mounts and usually if state changes were to occur at other areas of the page or the component the use query hook wouldn't need to make another query.
[12:26 - 12:53] However when the values of the variables in the use query hook changes it recognizes that it should make another query to get the fresh new data from the server. And lastly when we toggle back and forth between the two different options after the second time you make the query will notice the query isn't being made again despite our UI showing the new changes.
[12:54 - 13:02] This is because of Apollo's state management solution. When a query is first made it caches the data from that query.
[13:03 - 13:18] By default if we attempt to re-query what we've already queried Apollo will check and be like hey we already have this data in the cache. I don't think we need to hit the network again so let's just provide the data from the cache directly.
[13:19 - 13:44] This behavior of querying data keeping it in state updating states from user action and if needed re-quaring information is super important and in my opinion is oft entimes a very large part of the work you'll do when building React applications. We'll see another very similar example of this pattern when we build the pag ination capability in this page.
[13:45 - 14:09] To show pagination elements we could just use the pagination prop object that the add design list component expects just like how we've done for the list of listings or bookings shown in the user page. Though very similar what we'll do in this case is construct our own component called listings pagination that uses the add design pagination components.
[14:10 - 14:34] The pagination we've seen before within a list essentially renders the same pag ination element so all the props we've used before can be used here as well. The props will declare would be the total amount of items in the list what the current page is the default page size or in other words the default amount of items in a single page and so on.
[14:35 - 14:52] So with that said we're interested in telling this pagination component what the total amount of results from the query is. This is because this pagination component uses that data to determine the amount of page elements to show within the entire element.
[14:53 - 15:37] The API we've set up allows us to retrieve this information so we'll head to the listings graph ql document we've set up and specify a total field to be returned alongside the region and results. To have our generated typescript definitions recognize that the listings data to be returned is to have a total number field we'll head to the terminal and run the npm run code gen generate command to regenerate our graph ql typings.
[15:38 - 15:55] In the listings page components we'll now create a state property responsible in tracking the page the user is viewing. We'll call this state property page and the function responsible in updating the state property as set page.
[15:56 - 16:41] Initially we'll always want the user to see the first page of results so we'll provide an initial value of 1 and we'll have the page state variable now be passed in as the value for the page variable in our query. We'll now create the listings pagination components that will have the markup for the pagination section so we'll create the appropriate folder and index file in the adjacent components folder and in the components index file we'll react sport the soon to be created listings pagination components.
[16:42 - 17:05] For this listings pagination components we'll only need to import two things the react library and the pagination component itself from ant design. This component is to accept a couple of props from the parent that will be used in the pagination element.
[17:06 - 17:16] It would accept the total number of listings that is retrieved from the query. It will accept the page or in other words the page currently being viewed.
[17:17 - 17:55] It will accept the limit or in other words the total amount of items that should be shown in a page and it will accept the function we'll call set page that will essentially be used in the parent to update the page state variable so it would accept a page argument of type number and will return void. We'll export the component function declare the props it is to accept and return the pagination component from ant design.
[17:56 - 18:15] There's a couple of things we'll do here to have the pagination component behave the way we will want it to. The current prop in pagination tells the pagination component what the current page is with which we'll provide a value of the page prop we're going to pass in.
[18:16 - 18:31] The total prop refers to the total amount of items and helps control the number of page elements to be shown in the entire pagination element. We'll also pass the total prop along for this as well.
[18:32 - 18:52] The default page size prop refers to the number of items to be shown in the page which is also used by the component to determine the number of page elements to be shown in the list itself. We'll supply the limit prop that we're going to pass in to the listings pag ination components.
[18:53 - 19:17] The hide on single page prop is useful since it allows us to tell the pag ination component to be hidden when there's only a single page which is what we'll want. The show less items prop helps show less page numbers and compresses them to focus more on the initial page numbers and the last page numbers in the list.
[19:18 - 19:30] This is mostly a preference thing but we'll apply it as well. And then there's the on change callback prop that will trigger when a page number is selected.
[19:31 - 19:46] We'll take that page number as the argument and pass it along to the set page function prop that will be available and passed in. And lastly we'll just add a class we've created to help with some minor styling .
[19:47 - 20:03] And now in the parent listings will import listings pagination from the adjacent components folder. And we'll now look to render the listings pagination component in the listings parent itself.
[20:04 - 20:19] Similar to the filters we wouldn't want to show any of the pagination elements only until the listings data is available and listings are actually present. So we'll look to place it among where the listings filter component is being rendered.
[20:20 - 20:28] So we'll place it right before the listings filters. We'll pass the necessary props that it is to accept.
[20:29 - 20:42] The total amounts can be determined from the total field from the listings object return from GraphQL. We'll pass the page state value as the prop for page.
[20:43 - 21:00] Limit will be the same limits we've specified in our query with which we have a constant called page limit defined for this. And we'll pass along the set page state function for the prop of the same name.
[21:01 - 21:13] And if we take a look at our listings page now we'll see nothing is being shown . However, keep in mind we've said the pagination component should only show if there's more than one page of results.
[21:14 - 21:27] Here in Toronto we have eight listings to be shown. So we can test out this pagination section or component by simply changing the page limit and we'll set it to four temporarily.
[21:28 - 21:43] This will tell our query to only return four listings at a time and we'll also tell the pagination component that the limit of page items will be only four for a single page. And if we are to have only eight listings we should see two pages.
[21:44 - 21:55] So now when we go back to the UI for our listings route we'll see the pag ination element. If we clicked page two we'll get a new result of listings.
[21:56 - 22:03] If we toggle back and forth we'll toggle between the listings from page one and page two. Amazing.
[22:04 - 22:13] Now what's happening here is almost very similar to what's happening when we change any of the filters. We have a state variable being used in the query.
[22:14 - 22:22] When a user action is made it updates the state variable. When a change in state variable happens react components re-render.
[22:23 - 22:45] Since the value of the state variable is being used in the use query hook when it changes it makes the query happen again. And finally once the data is available in Apollo's cache the next time we try to visit a certain page Apollo will get the data from the cache by default as opposed to making the network request.
[22:46 - 22:54] Cool. Now let's go back and make sure our page limit constant value is back to set to eight.
[22:55 - 23:18] One other minor thing we'll do around this area is use and designs a fix component to affix the filter and pagination sections to the top of the page if the user was to scroll past below the entire sections. We've used the affix component in the header of our app and it's fairly straightforward to use.
[23:19 - 23:43] First we'll import it from end design and we'll wrap our listings filters and listings pagination components with the affix components. We'll specify an offset top prop which helps offset the section or the children within from the top of the page if the user was to scroll below them.
[23:44 - 24:08] We'll apply a value of 64 and this would help position the filters and pag ination sections below the app header by around 64 pixels from the top if the user was to scroll below the sections. And now when we take a look at our listings page and if we were to scroll down we'll see the filter section remain affixed close to the top.
[24:09 - 24:14] And if the pagination element was to show it will be within the same affixed section as well. Great.
[24:15 - 24:35] The last couple of things we'll tackle in this page are the UI elements we'll show when the query is either in the loading state or has erred out completely. We'll first do the loading parts with which we'll create a component for this called listings skeleton.
[24:36 - 24:53] We'll create the listings skeleton component folder and the associated index file. And we'll have it re-exported from the components index file in the listings module.
[24:54 - 25:08] We've mentioned we want the loading state of this listings page to be very similar to that of the loading we show for the listings shown in the home page. That will want to show 8 loading cards instead.
[25:09 - 25:22] Now for this loading behavior we are to use a grey cover image for our loading cards. So we've added this particular image asset to the assets folder of the listings component module.
[25:23 - 25:48] We'll provide this asset in the project source code we'll share with you and as a link in the lesson manuscript. Since this listings skeleton component is to be very similar to this skeleton component we show for the listings in the home page, let's head over to the home listings skeleton file and we'll copy over the contents and we'll make changes as we go.
[25:49 - 26:13] Everything will mostly remain the same except we'll rename the components function accordingly. We'll make a few changes to some of the class names being stated and in the skeleton paragraph element being rendered or returned we'll say we want to show a single row here.
[26:14 - 26:28] And remember how we've mentioned we want to show 8 cards or 8 loading cards not 4. The way we achieve this is we have the list in this component render 8 times as opposed to 4 times.
[26:29 - 26:47] And we've used an empty data constant array that consists of empty objects to help render the list item a certain number of times. So we'll update this empty data array to contain 8 objects so we'll render the list items 8 times.
[26:48 - 27:23] And the listings component file will import listings skeleton from the adjacent components folder and will destruct the loading state from the use query hook and we'll say when loading is true we'll simply render the listings skeleton component directly and we'll keep it within antdesigns contents components. And there we have it.
[27:24 - 27:46] Now when our query is loading we'll see the loading skeleton be presented to us that mimics the perceived outcome of the page which makes it look pretty good. The last thing we'll handle is if our query was to ever fail and if it was we 'll show the skeleton but we'll also show an error banner at the top as well.
[27:47 - 28:40] So we'll import the error banner components we've created in the lib components folder that we use for the error banners in our app and we'll destruct the error property from the use query hook and we'll say when error is ever true we'll render the listings skeleton similar to how we do it in the loading condition but we'll also place the error banner components above the skeleton with a description that says we either couldn't find anything matching your search or have encountered an error. If you're searching for a unique location try searching again with more common keywords.
[28:41 - 29:01] Now as a note for both this error and loading state we could use a single if condition with an or statement to determine whether the error banner should show or not however this outcome is pretty much the same as doing that as well. So now let's test out this error ever happening.
[29:02 - 29:40] Now an error could occur if the server was to just error out or the network connection we have just becomes lost however what we'll do is we'll try and search for a location that would most likely cause the geocoder to not be able to find a country at all with which we've said the server will ever. So we'll type a random set of gibberish or gibberish characters in the route for the URL parameter and then when you press enter if the geocoder wasn't able to pick up what we're trying to search for our query would ever.
[29:41 - 29:51] We're presented with the loading skeleton and an error banner up top telling the user that something might have went wrong. Fantastic and there we have it.
[29:52 - 30:00] Our listings page at this moment in time is pretty much complete. We have the capability to query listings entirely.
[30:01 - 30:29] We have the capability to query listings for certain locations. When a query is made and successful we're able to filter the listings shown to us whether it's from lower price to higher price listings or vice versa and if there are a large number of listings which basically mean a large number of pages the user is able to use the pagination elements available to them to migrate or navigate from page to page.
[30:30 - 30:30] Great job so far.