Build User Profile Pages With GraphQL, Ant Design, & Apollo

In this lesson, we'll continue to build the user page in our client application by looking to query and present a paginated list of listings and bookings for a certain user.

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To 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 \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two

In the last lesson, we've been able to query for a single user and display that user information in the user page. In this lesson, we'll now look to query a paginated list of listings and book ings and display that as well in the user page. Listings here refers to the listings a user has created and bookings refer to the bookings the user has made to other listings. Listings will be unprotected. That is to say with which we'll be able to see when we navigate to any user page. The bookings section on the other hand will be authorized where only the viewer viewing their user page will be able to see. This listings section will be a child user listings component of the user page. The bookings section will be a child user bookings component of the user page. Other listings and user bookings resemble one another due to the cards that they both show. The UI for these cards are going to be used in many different areas of our app including the home page and the listings page. As a result, we'll create a listing card components to resemble this card which will be part of our application's lib folder. The listing card component will be fairly straightforward. It'll accept a series of props such as the price of a listing, the title, description, number of guests and it will display that information. Let's get started. The first thing we're going to do is update the user query in our app to now query for the bookings and listings fields of a user. Bookings and listings are paginated fields that require us to pass a limit and page arguments. We're going to be passing these arguments from our components so let's state that the user query is going to accept a few new arguments. We'll be explicit and state that is going to accept a bookings page argument that will determine which booking page the user is in. It'll be of an integer type. It'll also accept a listings page argument that will determine which listings page the user is in. We'll want to show up to four bookings or listings in a page so we can probably hard code this limit values in the fields but we'll have our components pass this along as well. Since booking and listings will have the same value here, we'll only pass a single limit argument. We'll now first query for the bookings field and pass the limit argument along and say that for the value of the page arguments will be the bookings page argument passed in our query. So now it might be hard to remember the schema of fields for certain object types so let's head over to GraphQL Playground to see the schema of the bookings field within user. If we recall, the bookings field in the user object will return a total field and a result field which is the list of bookings objects. We'll need both in the UI. So if we take a further look at the booking object type within the bookings field, we'll need the ID, listing, check in and check out fields. The user querying this field is the tenant so we won't need to query for this in the user page. And for the listing field, we don't need all the information for the card. Instead, we'll only need a few fields since the card is intended to be a summary of the listing. We'll just need the ID, title, image, address, price and number of guests fields. So let's go back to our client application and query the fields we want. Total, the result which contains an ID, the listing, ID, title, image, address, price, number of guests and the check in and check out fields. The listings field is going to be very similar except that there's no check in checkout information within a listing object type here. Instead, result is the list of listing objects where we'll get the ID, title, image and other information we're looking for. So we'll query for the information we want and we'll also ensure that we're passing the listings page argument value for the page argument in the listings field. There won't be a reason for us to update the schema we have in our client application since the schema has remained the same, but we'll now update the auto-generated type definitions in our client. We'll head to the terminal and we'll run the codegen generate command. With our type definitions now updated, the query we're making in the user page will now throw an error since we're not passing in the additional variables the query now accepts. We'll come back to this in a second. For now, we'll look to create the custom listing card components that are upcoming user listings and user bookings components are going to use. We'll create this component in the live components folder. And in the components index file, we'll re-export the listing card component we 'll shortly create. The listing card components will be mostly presentation UI and will be fairly straightforward. Let's first import the components we'll need to use from Add Design, but card, icon and typography components. We'll say the listing card component is to accept a single listing object prop, which will have an ID, title, image and address fields to be of type strings, as well as price and number of guest fields, which are to be numbers. Will destruct the text and title components from the typography component? Will create and export the listing card components? Will destruct the listing prop from the props argument? And will further destruct the field values within the listing object prop. And in the components return statement, we'll simply return the card component from Add Design. In the card component cover prop, we'll state the background image of the cover is the listing image and the rest of the component markup will display information for the listing price. Also, address and number of guests. We'll go back to the user component. With which we've given a value of user to get the user icon. Okay, this will be pretty much our listing card components. We'll be making some minor changes to this when we survey and see how it behaves in our client application, so we'll come back to this in a second. We'll head over to the user component and look to update the query for user being made. The query for user now expects three new variables. The bookings page of the user, the listings page of the user and the limit value, which is to be the value of the number of items we can see in a page for bookings or listings. For the bookings and listings page values, we want our components to keep track of this value and update it based on which of the pages the user wants to visit. As a result, these values will be best kept in state. So we'll import the use state hook from react and at the top of our listings components, we'll use the use state hook to create two new state values. Bookings page and listings page. We'll initialize these page values with the value of one, since when the user first visits the user components, we'll want them to see the first page of the bookings and listings lists. We'll also destruct the functions that will be used to update these state values. Set listings page and set bookings page. Since the limits value would always stay the same and we won't want the user to update this, we'll create a constant above our components called page limits that will reference the limit of items in a bookings or listings page, which will be four. In our use query hook declaration, we'll now pass the new arguments in for book ings page, listings page and limits. We'll now look to see how the user components will render the user listings and user bookings components before we create them. We'll first check that if the user object exists and if so, we'll assign the listings and booking fields of the user data to the constants, user listings and user bookings. Similar to the user profile element constant and create constant elements for the user listings and user bookings components. If listings exist, we'll have a user listings element be the user listings component. Before the user listings component, we'll want to pass in a few props that the component will eventually use, which would be the user listings itself, the listings page value, the limit value and the function necessary to update the listings page value, set listings page. The user bookings element will create, it'll be very similar except it's going to reference the user bookings component and it's going to pass the user bookings list, the bookings page and the set bookings function as props. And now in the user component return statement, we'll simply render the user listings element and user bookings element within their own column. Now look to create these user listings and user bookings components as child components within user. We'll create their folders in the components folder within user and in the components index file, we'll re-export the user listings and user bookings components that we'll create shortly. We'll begin with the user listings component. The main component we're going to use from ant design to help create this list is the powerful list component. In the documentation, ant design shows us many different ways we can render a list with the list component. In particular, we're interested in this card format we have here. If we take a look at the code example, we can see the list component takes a grid prop that helps control the structure of the grid. It takes the data source prop, which is essentially the list of data that's going to be iterated and displayed. It takes a render item prop, which is a prop function that determines how every item in the list is going to be rendered. In this example, they simply render a card that says card content with the appropriate card title, but we'll be interested in rendering our own custom listing card components and we'll pass in the prop that that component would expect. Also, though not shown in any example code here, when we look at the API list, there also exists a pagination prop that helps set up the pagination configuration for the list. The object that this prop expects is adapted from ant design's pagination component and it allows us to specify information like the current page, the total number of items in the list, the default page size and so on. We'll see what these prop fields are when we start to create our list components. In the user listings component file, let's begin by first importing what we'll need. We'll import the list and typography components from ant design. We'll import the listing card components from our lib components folder. We'll also import the auto-generated user interface for the user data being queried. We'll then declare the props that this component is to accept. User listings, listings page, limit and set listings page. Listings page and limit will be numbers, while set listings page will be a function that accepts a number argument and returns void. If we take a look at the auto-generated user interface, we're interested in accessing the type of the listings field within user, within the user interface. So we'll use the capability of using lookup types like we've seen before to access the listings interface type for the user listings prop. We'll also destruct the paragraph and title child components from typography. We'll create and export the user listings component and destruct the props we 'll need. We'll further destruct the total and result fields from the user listings prop object. Let's create the list elements within its own constant we'll call user listings list and we'll use ant designs list components. We'll explain each of the props we'll use as we declare them. Grid will be used to help set up the grid layout in different viewports. The gutter field helps introduce some spacing between columns, so we'll give it a value of 8. The extra small field dictates the amount of columns to be shown in extra small viewports. We'll just say 1. For small viewports, we'll want to see 2 columns and for large viewports, we'll want to see 4 columns. Data source is the list data that we want to pass in. We'll pass in the result array from our user listings object which is the list of listings. Local can help us introduce text for empty lists, so we'll follow the format add design expects and pass an object with an empty text field that will say user doesn't have any listings yet when the user doesn't have any listings. Appellation is how we'll set up the pagination element of our list. We'll pass an object and the fields we'll want to configure this. Position top helps position the pagination element at the top. Current references the current page with which we'll give a value of the listings page prompt. Total references the total amount of content with which we'll give a value of the total field from our query, default page size, helps determine the default page size with which we'll pass a value of the limits which is 4. Hide on single page helps us hide the pagination element when there's only one page. We'll want this so we'll give this a value of true. Show less items helps construct the pagination element in a way that not all page numbers will be shown for very large lists. We'll only show the pages around the current page and the boundary pages with which we'll want as well so we'll pass a value of true. And finally here there's an onChangeCallback function that runs when the user clicks a page number. Here we'll take the payload of the callback and run or call the setListingsPage function we have that will update the listings page state value in the parent. The last prop will declare in the list component is the renderItem prop which takes every item within the data source and determines the UI for each list item. We'll keep it simple and say that we'll want to render a listing card with which we'll pass along the iterated listing item as props and we'll declare this listing card to be rendered within the item component within list. With our list created we can now render it in our components return statement. We'll return some markup that provides a title of listings and a description for what the listing section is going to be and we'll return the userListingsList element. This will be the UI we'll need for our userListings components. The user Bookings component will be very similar to this so we'll copy the contents of this file over to the user Bookings index file and make the necessary changes. We'll reference the appropriate props user Bookings will receive, user Bookings page and the set Bookings page function. We'll destruct text among paragraph and title from the typography component which we'll eventually use and we'll rename the component to user Bookings. We won't be able to destruct the total and result value from user Bookings directly since user Bookings might be null and typescript will give us a warning. User Bookings is null when a viewer views the user page of another user so we 'll need to handle this. So instead what we'll do here is use turnary statements to determine the values of total and result constants which will be null if user Bookings is ever null. We'll create an element for the list as well but we'll call this list user Book ings list. This element however will be conditionally shown only when the total and result constant values exist which means that the user Bookings prop has data. Our list will appear similar except that we'll now reference the appropriate prop values in the pagination object Bookings page and set Bookings page. In the render item function one of the changes we'll make is that we'll now display booking history over the listing card and we have this data since every booking item list will provide information on the check in and check out dates of the tenant. So we'll create this booking history element within a constant that will simply show the check in and check out values. And we'll render this element above our listing card. Instead of returning UI directly what we're going to do is have an element conditionally contain the entire booking section that will contain the user Bookings list if the user Bookings prop is available. We will change the title and description to now reference that this is Bookings information. And finally we'll render or return this conditional element as part of the components return statement. If user Bookings list is ever null our user Bookings components will now return nothing that is to say no. If user Bookings list is populated our user Bookings components will render content. We can also have this conditional check in the parent but we'll have it within this component itself. And that's it, we've created the user listings and user Bookings components. In the user parent's component let's import these newly created child components and save our file. Let's now look at our application and see how our user page currently looks. We see the listing section which basically tells us that we're showing this information despite that no listings data exists but we should also see the booking section but we're not seeing it in here for some reason or another. Keep in mind that at this moment in time I'm authorized since I'm logged in and since I'm authorized the UI we intend to build is to show the booking section and if there's no booking data available we want to see the empty text that basically tells us that hey you haven't created any bookings yet. So we take a look at our code and let's just see how we conditionally show the bookings list section. So we say user Bookings list can only be shown if the total and result values exist. However keep in mind total here is a number. So what we've done here is even though bookings field might be returning data if there's no list information within booking data total will equal to zero and as a result what we've done here is basically have the user bookings list section be no. If user bookings list has been no we basically said the user bookings element section would be no. So let's update this slightly. What we'll do instead is we'll say user bookings list should be shown as long as the user bookings prop data exists. And then TypeScript will complain in a few instances and we'll make the minor modifications to specify that if result exists past the result a data source and if the total value exists make total the value for the total field in our pagination objects. If we now take a look at our UI we'll see the bookings section be shown. Great. As we can see at this moment in time we don't have any listings or bookings data for my user and we haven't created the capability to add listings or make bookings. So to verify that at least our user listings component works as intended we could introduce mock data in my user document in the users collection. But what we'll do instead is we'll grab an ID of a user from our mock users collection and try to visit their user page directly based on the routes. By doing so we now see an error. Why is this error being shown? If we take a look at our network tab and see what the GraphQL server returns in the errors array it tells us that it can't return null for the non-nullable field listing ID. But we haven't defined any resolver functions for the listing object type in our server. Let's move to our server project code briefly to understand what could be causing this issue. We've defined resolver functions for the user object appropriately and the listings and bookings fields in our resolvers resolve to the listing and booking object types. But we haven't defined any resolver functions for the listing and booking object types. This is fine for trivial resolver functions that our server implementation can handle for us. However, we'll need to set up resolver functions for the fields that need to be handled. One of these fields is the ID field with which we've seen before. So let's create the listings resolver map in the listings resolver file for the listing object type. And for the ID resolver, we'll simply reference the underscore ID field of the ID. In this instance, the listing document is different than the user document in our database, since underscore ID in the listing document is of type object ID, not a string. So we'll use the two string helper to ensure that is to be of type string. In our resolver's index file, we'll import the listings resolver's map and place it in the merge function. There's going to be other resolver functions we'll eventually need to create for the listing object. But for the fields we need for the user page, the ID resolver is the only one we'll need to create. The other fields we're querying are being handled as trivial resolvers. We'll also need to create a bookings resolver's map and some resolver functions for a few fields we're querying from the booking objects. Similarly, we'll create a bookings resolver file where we'll export a bookings resolver's map object. We'll specify the ID resolver just like we've done for the listings resolver's map. There's one other field we're querying for in our client from the booking object that will also need an explicit resolver function. We're querying for the listing field from the booking object. In the booking document, we store listing as IDs, but in the client we expect listing objects. So we'll create a resolver for the listing field in the booking object to find a single listing document from the listings collection where the underscore ID field is equivalent to the ID of the booking listing field. We'll now import the bookings resolver's map in our resolvers index file and place it in the merge function. If we go back to the booking resolver's map, let's ensure that we're specified the types of the return of each function. We've done so for the ID function, but not for the listing function. This listing function will be a promise that when resolved, we'll essentially return either a listing interface type or no. In this instance, we'll now import the listing interface from the lib types file. And those are the resolver functions we needed to create. If we went back to our client application, knowing that both our server and client code are running again and refresh the route that we wanted to access for another user, we can see that we now actually see a listing section. We're able to see pricing information, the title, the number of guests, but we don't see an image, so there's another mistake we must have made here. This is probably most likely attributed to the fact that we may have not introduced a certain class in one of the div elements. So we'll go back to our listing card component. And I think the mistake has been the class name introduced for the card. This particular class we created earlier was for the actual cover image. So we'll remove the class name declared for this card element here. And for the div elements for the cover, we'll replace the class name list card cover image class that we've created for the card cover. Saving our file and now going back to the client application, we're now presented with some lovely listing images for each listing that this user has. This is almost where we'll want our user component to be. We can't see booking information for this user since we're not authorized to, but it will look very similar to the listing section here. There's a few more improvements we'll need to make before we discuss one interesting point and close this lesson. All pricing information stored in our database is in sense, and we want to display it in dollar format, so we'll need to create this functionality. In addition for the icon shown in listing card, we'll like to show a nice blue color that matches the primary color being used in our application. We're going to need a function to format the currency and the value of the blue color in a few different areas of our app later on. So we'll create this in a shared libutels file where we keep shared app functionality. We'll create a format listing price function that will take two arguments, price, which is a number and a round boolean, which will default to true. In most areas of our app, except for a few, we'll want to have the currency formatted to dollars and have it rounded to a whole number since it's more presentable that way. This is why we'll have the round property as a function argument to control whether we want the number rounded or not. The format listing price function will be fairly simple and will simply return a string containing the dollar symbol and a format listing price value, which will get the dollar value and be rounded depending on the value of the round arguments. We'll also export an icon color constant, which will simply be a string to represent the primary hex color of our app. In our listing card components, we'll now import the icon color and format listing price function from the libutels file. We'll use the format listing price function to show the price. By not passing in a second argument, it will be rounded to a whole number and will apply a style to the icon element where we'll specify the color to be the icon color value. Lastly, we want listing cards in any area of our app, whether it's the user page, the home page, the listings page and so on, to be linked to the actual listing page of that particular listing. To make this happen, we'll import the link components from React Router DOM. We'll destruct the ID of a listing from the listing prop object and we'll wrap our return statement with the link component with a target route of listing ID. ID in the listing route will now be the dynamic match parameter, just like we 've seen in the user page. When we now take a look at our client application, we can see that the pricing details now show the dollar symbol. There are dollars per day. The icon that shows a number of guesses in the light blue color. We don't want both the title and the address fields to both be bolded in the listing card, so we'll go back to the listing card component and we'll remove the second strong prop for the address field. We'll go back to the client now and the listing card is pretty much where we'd want it for any area we'll need to use it in our app. In addition, these listing cards are now links. We'll need to click a particular card here, we're taken to the listing page with the appropriate ID of this listing. And we can do this for any listing card that we actually see. Amazing. There's something we should consider and talk about and that is how pagination behaves in the listings section and similarly we'll behave in the booking section. We can see that no pagination element exists here, which is most likely due to the prop field hide on single page that we added to the pagination prop from @designs list component that basically says we shouldn't show the pagination element when only a single page of elements exists. So let's find another user ID from the user's collection where the user might have more than a single page of listings. Like this user. Now we can see the pagination element and if we were to click another page, our user page will be loaded once again and now we'll be in another page of listings. Amazing. But wait a second, how is this working? If the user page is loading, this probably means we're making another query request and we can confirm this from our browser's network tab. But why is another query request happening? When we take a look at the use of the use query hook in the user component, we 've come to understand that the use query hook makes the query request when the component mounts for the very first time. When we click a new page in the pagination element, the only thing that's actually being changed is the value of the listings page or booking page state. The use query hook in ReactorPolar is smart enough to make another query request when the value or the values of the variables of the query changes. And this occurs by default. How cool is that? This is especially cool because our component is prepared to consume the data, loading and error properties of our query result. When our query is in flight again, loading is set to true once again and our page shows the loading skeleton and when it completes, we see the new data, which is the new list of listings and the new list of bookings. There's another important note we should talk about. If we head back to the client and now try to navigate to a listing page we've already visited, our UI would update the listings being shown but a request won't be made again. How is this working? This is due to something we've briefly mentioned before but now can actually witness. A polar client doesn't only give us useful methods to conduct data fetching but it also sets up an intelligent cache without any configuration on our part. How does this cache work? When we make requests to retrieve data with a polar client, a polar client under the hood caches the data in the UI in an area we can call the cache. The next time we return to the page that we've just visited, a polar client is smart enough to say hey, we already have this data in the cache. Let's just provide the data from the cache directly without needing the client to make another request to the server. This saves time and helps avoid the unnecessary re-request of data from the server that we've already requested before. This would remain regardless of how we would want to navigate within our application. Like if we were to head to the home page and if we had a link back to this user page, the data that we've already retrieved will then be retrieved from the cache. A polar client even gives us the capability to directly update information in the cache. But that's not really intuitive and it takes a little time to get used to. Now is there ways we can tell a polar client to force the request from the network and not from the cache? Yes. And the primary way of doing so is using the fetch policy option that can be used within our GraphQL queries. This is the Apollo documentation that it tells us that the fetch policy is an option which allows you to specify how you want your component to interact with the Apollo data cache. By default, your components will try to read from the cache first and if the full data from the query is in the cache, then Apollo simply returns the data from the cache. If the data isn't in the cache, then Apollo will always execute the request using the network interface. And these are some of the valid fetch policy values. Cache first is the default value and as we've just said, is where Apollo immediately in the beginning attempts to read data from the cache and if data does not exist, only then make a network request. However, there are other options like cache and network where in this particular case, the fetch policy would have Apollo first trying to read data from the cache and if all the data needs to fulfill the queries in the cache, then the data will be returned. However, regardless of whether the data is in the cache or not, this particular policy will always execute the query with the network interface. And this particular policy optimizes for users getting a quick response from the cache while immediately then making a network request at the extra step to see if there's any additional information. There's also the network only approach where it will never get information from the cache. There's the cache only approach where it will never get information from the network. And then there's the no cache approach in which the fetch policy would never return data from the cache as well. Personally, we always tend to use either the cache and network approach or stick with the default approach. The default approach is what we leave it as is primarily until we see an issue exists where we need to get information from the network. The cache and network approach as well will then use because at this particular case, it would immediately get information from the cache which tells the user the information they're looking for and it will quickly make an update once the network request is complete. And if the network was ever to fail, at least the cache information is presented. We'll talk about this some more closer to the end of the course. Okay, we're going to stop here for now. Great job so far. The UI we've built for this user page would be used again or be very similar to the other pages we've built in our application. [ Silence ]