The UserListings & UserBookings React Components
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.
📝 Documentation on the
<List />
component we use from Ant Design can be found - here.
In the last lesson, we've been able to query for a single user and display some of that queried information in the /user/:id
page of that user. In this lesson, we'll look to query the paginated list of listings
and bookings
for the user and display it on the user's page.
The listings section will be a child
<UserListings />
component of the user page.The bookings section will be a child
<UserBookings />
component of the user page.
<UserListings />
and <UserBookings />
resemble one another based on the cards being shown. The UI for these cards is going to be used in many different areas of our app including the /home
page and the /listings/:location?
page.

Since these listing card elements are going to be used in multiple areas of our application, we'll create a <ListingCard />
component to represent a single listing element in our application's src/lib/components/
folder that can be used anywhere. The <ListingCard />
component will be fairly straightforward - it will accept a series of props such as the price
of a listing, the title
, description
, numOfGuests
, and it will display that information within a card.
Update User Query#
The first thing we're going to do is update the user
query document in our app to query for the bookings
and listings
fields of a user
. bookings
and listings
are paginated fields that require us to pass in a limit
and page
arguments. We're going to be passing these arguments from the <User />
component that makes the query so let's state the User
query document we've established in the src/lib/graphql/queries/User/index.ts
file is going to accept a some new arguments.
We'll state that the User
query is going to accept a bookingsPage
argument that will determine which booking page the user is in and will be an integer type. The User
query will also accept a listingsPage
argument that will determine which listings page the user is in. Since we'll have the same limit
for the number of bookings
or listings
that can be shown on a single page, we'll only specify a single limit
argument is to be passed into the User
query.
import { gql } from "apollo-boost";
export const USER = gql`
query User($id: ID!, $bookingsPage: Int!, $listingsPage: Int!, $limit: Int!) {
#...
}
`;
bookings
#
In our user
query statement, we'll now query for the bookings
field and pass the limit
argument along for the limit of bookings we want in a single page. We'll also say the value for the page
argument for the bookings
field will be bookingsPage
.
import { gql } from "apollo-boost";
export const USER = gql`
query User($id: ID!, $bookingsPage: Int!, $listingsPage: Int!, $limit: Int!) {
user(id: $id) {
id
name
avatar
contact
hasWallet
income
bookings(limit: $limit, page: $bookingsPage) {}
}
}
`;
We'll now query for the fields we'll want from within the bookings
field.
We'll query for the
total
field to get the total number of bookings that are returned.We'll query for the
result
field which is the actual list of booking objects. In each booking object, we'll query for:The
id
of the booking.The
listing
of the booking which we'll further query for theid
,title
,image
,address
,price
, andnumOfGuests
of the listing.The
checkIn
andcheckOut
dates of the booking.
import { gql } from "apollo-boost";
export const USER = gql`
query User($id: ID!, $bookingsPage: Int!, $listingsPage: Int!, $limit: Int!) {
user(id: $id) {
id
name
avatar
contact
hasWallet
income
bookings(limit: $limit, page: $bookingsPage) {
total
result {
id
listing {
id
title
image
address
price
numOfGuests
}
checkIn
checkOut
}
}
}
}
`;
listings
#
The listings
field we'll query from the user
object is going to be very similar to what we query for the bookings
field except that there's no checkIn
and checkOut
information. The result
field of listings
is the list of listing objects we'll query where we'll get the id
, title
, image
, address
, price
, and numOfGuests
of the listing. We'll also ensure we provide the limit
and listingsPage
values for the limit
and page
arguments the listings
field expects.
With all these changes, the USER
query document we've established will look like:
import { gql } from "apollo-boost";
export const USER = gql`
query User($id: ID!, $bookingsPage: Int!, $listingsPage: Int!, $limit: Int!) {
user(id: $id) {
id
name
avatar
contact
hasWallet
income
bookings(limit: $limit, page: $bookingsPage) {
total
result {
id
listing {
id
title
image
address
price
numOfGuests
}
checkIn
checkOut
}
}
listings(limit: $limit, page: $listingsPage) {
total
result {
id
title
image
address
price
numOfGuests
}
}
}
}
`;
There shouldn't be a reason for us to update the schema in our client application since the schema has remained the same from the last lesson but we'll now update the autogenerated type definitions for our GraphQL API in our client.
We'll head to the terminal and run the codegen:generate
command in our client project.
npm run codegen:generate
<ListingCard />
#
With our autogenerated type definitions updated, the query we've established in the <User />
component will currently throw an error since we're not passing in the additional variables the query now accepts (bookingsPage
, listingsPage
, and limit
). We'll come back to this in a second. First, we'll create the custom <ListingCard />
component that our upcoming <UserListings />
and <UserBookings />
components are going to use.
We'll create the <ListingCard />
component in the src/lib/components/
folder.
client/
// ...
src/
lib/
components/
// ...
ListingCard/
index.tsx
// ...
// ...
In the src/lib/components/index.ts
file, we'll re-export the <ListingCard />
component we'll shortly create.
export * from "./ListingCard";
The <ListingCard />
component we'll create will mostly be presentational and display some information about a single listing. In the src/lib/components/ListingCard/index.tsx
file, we'll first import the components we'll need to use from Ant Design - the Card
, Icon
, and Typography
components.
We'll expect the <ListingCard />
component to accept a single listing
object prop which will have an id
, title,
image
, and address
fields all of which are of type string
. The listing
object prop will also have a price
and numOfGuests
fields which are to be number values.
import React from "react";
import { Card, Icon, Typography } from "antd";
interface Props {
listing: {
id: string;
title: string;
image: string;
address: string;
price: number;
numOfGuests: number;
};
}
We'll destruct the <Text />
and <Title />
components from the <Typography />
component. We'll create and export the <ListingCard />
component function and in the component, we'll destruct the field values we'll want to access from the listing
object prop.
import React from "react";
import { Card, Icon, Typography } from "antd";
interface Props {
listing: {
id: string;
title: string;
image: string;
address: string;
price: number;
numOfGuests: number;
};
}
export const ListingCard = ({ listing }: Props) => {
const { title, image, address, price, numOfGuests } = listing;
};
In the <ListingCard />
component's return statement, we'll return the <Card />
component from Ant Design. In the <Card />
component cover
prop, we'll state the backgroundImage
of the cover is to be the listing image. The rest of the contents within the <Card />
component will display information of the listing
such as its price
, title
, address
, and numOfGuests
.
import React from "react";
import { Card, Icon, Typography } from "antd";
interface Props {
listing: {
id: string;
title: string;
image: string;
address: string;
price: number;
numOfGuests: number;
};
}
const { Text, Title } = Typography;
export const ListingCard = ({ listing }: Props) => {
const { title, image, address, price, numOfGuests } = listing;
return (
<Card
hoverable
cover={
<div
style={{ backgroundImage: `url(${image})` }}
className="listing-card__cover-img"
/>
}
>
<div className="listing-card__details">
<div className="listing-card__description">
<Title level={4} className="listing-card__price">
{price}
<span>/day</span>
</Title>
<Text strong ellipsis className="listing-card__title">
{title}
</Text>
<Text ellipsis className="listing-card__address">
{address}
</Text>
</div>
<div className="listing-card__dimensions listing-card__dimensions--guests">
<Icon type="user" />
<Text>{numOfGuests} guests</Text>
</div>
</div>
</Card>
);
};
This will pretty much be the entire <ListingCard />
component. We'll make some minor changes to it when we survey and see how it looks in our client application.
Icon
is a useful component from Ant Design that provides a large list of icons that can be accessed by simply providing a value for the icon'stype
prop. In<ListingCard />
, we've used the<Icon />
component and provided atype
value ofuser
to get the user icon.
<User />
#
We'll head over to the <User />
component and first look to update the user
query being made. The query for the user
field now expects three new variables - bookingsPage
, listingsPage
, and limit
. For the bookings
and listings
page values, we want our <User />
component to keep track of these values and update them based on which of the pages the user wants to visit. As a result, these values will be best kept as component state so we'll import the useState
Hook in our <User />
component file.
import React, { useState } from "react";
At the top of our <Listings />
component, we'll use the useState
Hook to create two new state values - bookingsPage
and listingsPage
. We'll initialize these page values with the value of 1
since when the user first visits the /user/:id
page, we'll want them to see the first page for both the bookings and listings lists.
We'll also destruct functions that will be used to update these state values.
export const User = ({ viewer, match }: Props & RouteComponentProps<MatchParams>) => {
const [listingsPage, setListingsPage] = useState(1);
const [bookingsPage, setBookingsPage] = useState(1);
// ...
};
Since the limit
value (i.e. the limit of the number of bookings or listings that should show for a single page) will stay the same and we won't want the user to update this, we'll create a constant above our component called PAGE_LIMIT
that'll reference the limit of paginated items in a page - which will be 4
.
In our useQuery
Hook declaration, we'll then pass the values for the new variables - bookingsPage
, listingsPage
, and limit
.
// ...
const PAGE_LIMIT = 4;
export const User = ({ viewer, match }: Props & RouteComponentProps<MatchParams>) => {
const [listingsPage, setListingsPage] = useState(1);
const [bookingsPage, setBookingsPage] = useState(1);
const { data, loading, error } = useQuery<UserData, UserVariables>(USER, {
variables: {
id: match.params.id,
bookingsPage,
listingsPage,
limit: PAGE_LIMIT
}
});
// ...
return (
// ...
);
};
We'll now have the <User />
component render the child <UserBookings />
and <UserListings />
components before we create them. In the <User />
component, we'll check for if the user data exists and if so - we'll assign the listings
and bookings
fields of the user data to the constants userListings
and userBookings
respectively.
// ...
const PAGE_LIMIT = 4;
export const User = ({ viewer, match }: Props & RouteComponentProps<MatchParams>) => {
const [listingsPage, setListingsPage] = useState(1);
const [bookingsPage, setBookingsPage] = useState(1);
const { data, loading, error } = useQuery<UserData, UserVariables>(USER, {
variables: {
id: match.params.id,
bookingsPage,
listingsPage,
limit: PAGE_LIMIT
}
});
// ...
const userListings = user ? user.listings : null;
const userBookings = user ? user.bookings : null;
// ...
return (
// ...
);
};
We'll create constant elements for the <UserListings />
and <UserBookings />
components. If the userListings
constant has a value (i.e. listings
within user
exists), we'll have a userListingsElement
be the <UserListings />
component. For the <UserListings />
component we want to render, we'll pass in a few props that the component will eventually use such as userListings
, listingsPage
, limit
, and the function necessary to update the listingsPage
value - setListingsPage()
.
// ...
const PAGE_LIMIT = 4;
export const User = ({ viewer, match }: Props & RouteComponentProps<MatchParams>) => {
const [listingsPage, setListingsPage] = useState(1);
const [bookingsPage, setBookingsPage] = useState(1);
const { data, loading, error } = useQuery<UserData, UserVariables>(USER, {
variables: {
id: match.params.id,
bookingsPage,
listingsPage,
limit: PAGE_LIMIT
}
});
// ...
const userListings = user ? user.listings : null;
const userBookings = user ? user.bookings : null;
// ...
const userListingsElement = userListings ? (
<UserListings
userListings={userListings}
listingsPage={listingsPage}
limit={PAGE_LIMIT}
setListingsPage={setListingsPage}
/>
) : null;
// ...
return (
// ...
);
};
We'll create a similar userBookingsElement
constant that is to be the <UserBookings />
component when the userBookings
property has a value (i.e. bookings
within user
exists). The <UserBookings />
component will receive the following props - userbookings
, bookingsPage
, limit
, and the setBookingsPage()
function.
// ...
const PAGE_LIMIT = 4;
export const User = ({ viewer, match }: Props & RouteComponentProps<MatchParams>) => {
const [listingsPage, setListingsPage] = useState(1);
const [bookingsPage, setBookingsPage] = useState(1);
const { data, loading, error } = useQuery<UserData, UserVariables>(USER, {
variables: {
id: match.params.id,
bookingsPage,
listingsPage,
limit: PAGE_LIMIT
}
});
// ...
const userListings = user ? user.listings : null;
const userBookings = user ? user.bookings : null;
// ...
const userListingsElement = userListings ? (
<UserListings
userListings={userListings}
listingsPage={listingsPage}
limit={PAGE_LIMIT}
setListingsPage={setListingsPage}
/>
) : null;
const userBookingsElement = userBookings ? (
<UserBookings
userBookings={userBookings}
bookingsPage={bookingsPage}
limit={PAGE_LIMIT}
setBookingsPage={setBookingsPage}
/>
) : null;
// ...
return (
// ...
);
};
In the <User />
component's return statement, we'll render the userListingsElement
and userBookingsElement
within their own <Col />
's. With all the changes made in the <User />
component, the src/sections/User/index.tsx
will look like the following:
import React, { useState } from "react";
import { RouteComponentProps } from "react-router-dom";
import { useQuery } from "@apollo/react-hooks";
import { Col, Layout, Row } from "antd";
import { USER } from "../../lib/graphql/queries";
import {
User as UserData,
UserVariables
} from "../../lib/graphql/queries/User/__generated__/User";
import { ErrorBanner, PageSkeleton } from "../../lib/components";
import { Viewer } from "../../lib/types";
import { UserBookings, UserListings, UserProfile } from "./components";
interface Props {
viewer: Viewer;
}
interface MatchParams {
id: string;
}
const { Content } = Layout;
const PAGE_LIMIT = 4;
export const User = ({ viewer, match }: Props & RouteComponentProps<MatchParams>) => {
const [listingsPage, setListingsPage] = useState(1);
const [bookingsPage, setBookingsPage] = useState(1);
const { data, loading, error } = useQuery<UserData, UserVariables>(USER, {
variables: {
id: match.params.id,
bookingsPage,
listingsPage,
limit: PAGE_LIMIT
}
});
if (loading) {
return (
<Content className="user">
<PageSkeleton />
</Content>
);
}
if (error) {
return (
<Content className="user">
<ErrorBanner description="This user may not exist or we've encountered an error. Please try again soon." />
<PageSkeleton />
</Content>
);
}
const user = data ? data.user : null;
const viewerIsUser = viewer.id === match.params.id;
const userListings = user ? user.listings : null;
const userBookings = user ? user.bookings : null;
const userProfileElement = user ? (
<UserProfile user={user} viewerIsUser={viewerIsUser} />
) : null;
const userListingsElement = userListings ? (
<UserListings
userListings={userListings}
listingsPage={listingsPage}
limit={PAGE_LIMIT}
setListingsPage={setListingsPage}
/>
) : null;
const userBookingsElement = userListings ? (
<UserBookings
userBookings={userBookings}
bookingsPage={bookingsPage}
limit={PAGE_LIMIT}
setBookingsPage={setBookingsPage}
/>
) : null;
return (
<Content className="user">
<Row gutter={12} type="flex" justify="space-between">
<Col xs={24}>{userProfileElement}</Col>
<Col xs={24}>
{userListingsElement}
{userBookingsElement}
</Col>
</Row>
</Content>
);
};
We'll now look to create the <UserListings />
and <UserBookings />
child components we render within <User />
. We'll create the folders for them in the components/
folder within src/sections/User/
.
client/
// ...
src/
// ...
sections/
// ...
User/
components/
UserListings/
index.tsx
UserBookings/
index.tsx
// ...
// ...
In the src/sections/User/components/index.ts
file, we'll re-export the soon to be created <UserListings />
and <UserBookings />
components.
export * from "./UserBookings";
export * from "./UserListings";
<UserListings />
#
We'll begin with the <UserListings />
component. The main component we're going to use from Ant Design to help us create <UserListings />
is the powerful <List />
component.
The properties we're interested in using from Ant Design's <List />
component is:
The
grid
prop that helps control the structure of the grid.The
dataSource
prop which would be the list of data that is going to be iterated and displayed in the list.The
renderItem
prop which is a prop function that determines how every item of the list is going to be rendered. We'll be interested in rendering the custom<ListingCard />
component for every item in the list.The
pagination
prop to help set-up the list's pagination configuration. Thepagination
prop in the<List />
component is adapted from Ant Design's<Pagination />
component which will allow us to specify the current page, the total number of contents, the default page size, and so on.
In the <UserListings />
component file (src/sections/User/components/UserListings/index.tsx
), 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 <ListingCard />
component from our src/lib/components/
folder. We'll also import the autogenerated User
interface for the user data being queried.
import React from "react";
import { List, Typography } from "antd";
import { ListingCard } from "../../../../lib/components";
import { User } from "../../../../lib/graphql/queries/User/__generated__/User";
We'll then declare the props that the <UserListings />
component is to accept - userListings
, listingsPage
, limit
, and setListingsPage
. listingsPage
and limit
will be numbers while setListingsPage
will be a function that accepts a number argument and returns void
. For the userListings
prop, we'll declare a lookup type to access the interface type of the listings
field within the User
data interface used to describe the shape of data from the user
query.
This page is a preview of TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two