THE USERLISTINGS & USERBOOKINGS REACT COMPONENTS
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  totalfield to get the total number of bookings that are returned.
- We’ll query for the  resultfield which is the actual list of booking objects. In each booking object, we’ll query for:- The  idof the booking.
- The  listingof the booking which we’ll further query for theid,title,image,address,price, andnumOfGuestsof the listing.
- The  checkInandcheckOutdates of the booking.
 
- The  
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:
client/src/lib/graphql/queries/User/index.ts
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.
client/src/lib/components/index.ts
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.
Iconis 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’stypeprop. In<ListingCard />, we’ve used the<Icon />component and provided atypevalue ofuserto 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.
client/src/sections/User/index.tsx
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:
client/src/sections/User/index.tsx
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.
client/src/sections/User/components/index.ts
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  gridprop that helps control the structure of the grid.
- The  dataSourceprop which would be the list of data that is going to be iterated and displayed in the list.
- The  renderItemprop 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  paginationprop to help set-up the list’s pagination configuration. Thepaginationprop 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.
We’ll also destruct the  <Paragraph />  and  <Title />  child components from  <Typography /> .
import React from "react";
import { List, Typography } from "antd";
import { ListingCard } from "../../../../lib/components";
import { User } from "../../../../lib/graphql/queries/User/__generated__/User";
interface Props {
  userListings: User["user"]["listings"];
  listingsPage: number;
  limit: number;
  setListingsPage: (page: number) => void;
}
const { Paragraph, Title } = Typography;
We’ll create and export the  <UserListings />  component function and destruct the props we’ll need in the component. We’ll further destruct the  total  and  result  fields from the  userListings  prop object.
import React from "react";
import { List, Typography } from "antd";
import { ListingCard } from "../../../../lib/components";
import { User } from "../../../../lib/graphql/queries/User/__generated__/User";
interface Props {
  userListings: User["user"]["listings"];
  listingsPage: number;
  limit: number;
  setListingsPage: (page: number) => void;
}
const { Paragraph, Title } = Typography;
export const UserListings = ({
  userListings,
  listingsPage,
  limit,
  setListingsPage
}: Props) => {
  const { total, result } = userListings;
};
We’ll first create the list element within a constant we’ll call  userListingsList  and we’ll use Ant Design’s  <List />  component. Here’s how we plan on using each of the props we attempt to declare in the  <List />  component.
grid
grid  will be used to help set up the grid layout in different viewports.
- The  gutterproperty ingridhelps introduce some spacing between columns so we’ll state a value of8.
- The  xsfield dictates the number of columns to be shown in extra-small viewports with which we’ll provide a value of 1.
- For small viewports (i.e. the  smgrid field), we’ll provide a value2to show 2 items for the entire viewport.
- For large viewports (i.e. the  lggrid field), we’ll provide a value of4to show 4 items for the entire viewport.
// ...
export const UserListings = ({
  userListings,
  listingsPage,
  limit,
  setListingsPage
}: Props) => {
  const { total, result } = userListings;
  const userListingsList = (
    <List
      grid={{
        gutter: 8,
        xs: 1,
        sm: 2,
        lg: 4
      }}
    />
  );
};
dataSource
dataSource  is the data we want to use for the list. We’ll pass in the  result  array from our  userListings  object which is the list of listings.
// ...
export const UserListings = ({
  userListings,
  listingsPage,
  limit,
  setListingsPage
}: Props) => {
  const { total, result } = userListings;
  const userListingsList = (
    <List
      grid={{
        gutter: 8,
        xs: 1,
        sm: 2,
        lg: 4
      }}
      dataSource={result}
    />
  );
};
locale
locale  can help us introduce text for empty lists. For the value of  locale , we’ll pass an object with an  emptyText  field that will say  "User doesn't have any listings yet!"  which will be shown when the user doesn’t have any listings.
// ...
export const UserListings = ({
  userListings,
  listingsPage,
  limit,
  setListingsPage
}: Props) => {
  const { total, result } = userListings;
  const userListingsList = (
    <List
      grid={{
        gutter: 8,
        xs: 1,
        sm: 2,
        lg: 4
      }}
      dataSource={result}
      locale={{ emptyText: "User doesn't have any listings yet!" }}
    />
  );
};
pagination
pagination  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 it at the top.
- 
currentreferences the current page with which we’ll give a value of thelistingsPageprop.
- 
totalreferences the total number of items to be paginated with which we’ll give a value of thetotalfield from our query.
- 
defaultPageSizehelps determine the default page size with which we’ll pass a value of thelimitprop (which is 4).
- 
hideOnSinglePagehelps us hide the pagination element when there’s only one page and since we’ll want it, we’ll give this a value oftrue.
- 
showLessItemshelps construct the pagination element in a way that not all page numbers are shown for very large lists. WithshowLessItems, only the pages around the current page and the boundary pages will be shown which is something we’ll want as well.
- Finally, in the  paginationoptions there exists anonChange()callback function that runs when the user clicks a page number. We’ll declare theonChange()callback function, take the payload of the callback (which will be the new page number), and run thesetListingsPage()function we have that will update thelistingsPagestate value in the parent.
// ...
export const UserListings = ({
  userListings,
  listingsPage,
  limit,
  setListingsPage
}: Props) => {
  const { total, result } = userListings;
  const userListingsList = (
    <List
      grid={{
        gutter: 8,
        xs: 1,
        sm: 2,
        lg: 4
      }}
      dataSource={result}
      locale={{ emptyText: "User doesn't have any listings yet!" }}
      pagination={{
        position: "top",
        current: listingsPage,
        total,
        defaultPageSize: limit,
        hideOnSinglePage: true,
        showLessItems: true,
        onChange: (page: number) => setListingsPage(page)
      }}
    />
  );
};
renderItem
The last prop we’ll declare in the  <List />  component is the  renderItem  prop which takes every item within the data source and determines the UI to be displayed for each list item. We’ll keep it simple and say we’ll render a  <ListingCard />  component for each item with which we’ll pass the iterated listing as props. We’ll declare the rendered <ListingCard />  within the  <List.Item />  component.
// ...
export const UserListings = ({
  userListings,
  listingsPage,
  limit,
  setListingsPage
}: Props) => {
  const { total, result } = userListings;
  const userListingsList = (
    <List
      grid={{
        gutter: 8,
        xs: 1,
        sm: 2,
        lg: 4
      }}
      dataSource={result}
      locale={{ emptyText: "User doesn't have any listings yet!" }}
      pagination={{
        position: "top",
        current: listingsPage,
        total,
        defaultPageSize: limit,
        hideOnSinglePage: true,
        showLessItems: true,
        onChange: (page: number) => setListingsPage(page)
      }}
      renderItem={userListing => (
        <List.Item>
          <ListingCard listing={userListing} />
        </List.Item>
      )}
    />
  );
};
With our list element created, we can render it in our  <UserListings />  component’s return statement. We’ll return some markup providing a title and a description for what this listings section is and we’ll return the  userListingsList  element. With all these changes, the  <UserListings />  component file will appear as the following:
client/src/sections/User/components/UserListings/index.tsx
import React from "react";
import { List, Typography } from "antd";
import { ListingCard } from "../../../../lib/components";
import { User } from "../../../../lib/graphql/queries/User/__generated__/User";
interface Props {
  userListings: User["user"]["listings"];
  listingsPage: number;
  limit: number;
  setListingsPage: (page: number) => void;
}
const { Paragraph, Title } = Typography;
export const UserListings = ({
  userListings,
  listingsPage,
  limit,
  setListingsPage
}: Props) => {
  const { total, result } = userListings;
  const userListingsList = (
    <List
      grid={{
        gutter: 8,
        xs: 1,
        sm: 2,
        lg: 4
      }}
      dataSource={result}
      locale={{ emptyText: "User doesn't have any listings yet!" }}
      pagination={{
        position: "top",
        current: listingsPage,
        total,
        defaultPageSize: limit,
        hideOnSinglePage: true,
        showLessItems: true,
        onChange: (page: number) => setListingsPage(page)
      }}
      renderItem={userListing => (
        <List.Item>
          <ListingCard listing={userListing} />
        </List.Item>
      )}
    />
  );
  return (
    <div className="user-listings">
      <Title level={4} className="user-listings__title">
        Listings
      </Title>
      <Paragraph className="user-listings__description">
        This section highlights the listings this user currently hosts and has
        made available for bookings.
      </Paragraph>
      {userListingsList}
    </div>
  );
};
This will be the UI we’ll need for our  <UserListings />  component.
<USERBOOKINGS />
The  <UserBookings />  component will be very similar to the  <UserListings />  component. In the  src/sections/User/components/UserBookings/index.tsx  file, we’ll import the  <List />  and  <Typography />  components from Ant Design, we’ll import the  <ListingCard />  component from the  src/lib/components/  folder, and we’ll import the autogenerated TypeScript interface for the data obtained from the  user  query. We’ll destruct the  <Text /> ,  <Paragraph /> , and  <Title />  from  <Typography /> .
We’ll also reference the appropriate props the  <UserBookings />  component is to receive which is to be  userBookings ,  bookingsPage ,  limit , and the  setBookingsPage()  function.
import React from "react";
import { List, Typography } from "antd";
import { ListingCard } from "../../../../lib/components";
import { User } from "../../../../lib/graphql/queries/User/__generated__/User";
interface Props {
  userBookings: User["user"]["bookings"];
  bookingsPage: number;
  limit: number;
  setBookingsPage: (page: number) => void;
}
const { Paragraph, Text, Title } = Typography;
export const UserBookings = ({
  userBookings,
  bookingsPage,
  limit,
  setBookingsPage
}: Props) => {};
In the  <UserBookings />  component, we won’t be able to destruct the  total  and  result  value from the  userBookings  prop directly since  userBookings  might be null.  userBookings  will be  null  when a viewer views the user page of  another  user.
At the beginning of the  <UserBookings />  component function, we’ll use ternary statements to determine the values of the  total  and  result  constants which will be null if  userBookings  is ever  null .
// ...
export const UserBookings = ({
  userBookings,
  bookingsPage,
  limit,
  setBookingsPage
}: Props) => {
  const total = userBookings ? userBookings.total : null;
  const result = userBookings ? userBookings.result : null;
};
We’ll then create an element for the list of bookings which we’ll call  userBookingsList . This element will only be conditionally shown if the  total  and  result  values exist. The list element we’ll create for  userBookingsList  will be very similar to that created in the  <UserListings />  component except that we’ll reference the appropriate prop values for the pagination object -  bookingsPage  and  setBookingsPage .
// ...
export const UserBookings = ({
  userBookings,
  bookingsPage,
  limit,
  setBookingsPage
}: Props) => {
  const total = userBookings ? userBookings.total : null;
  const result = userBookings ? userBookings.result : null;
  const userBookingsList = userBookings ? (
    <List
      grid={{
        gutter: 8,
        xs: 1,
        sm: 2,
        lg: 4
      }}
      dataSource={result ? result : undefined}
      locale={{ emptyText: "You haven't made any bookings!" }}
      pagination={{
        position: "top",
        current: bookingsPage,
        total: total ? total : undefined,
        defaultPageSize: limit,
        hideOnSinglePage: true,
        showLessItems: true,
        onChange: (page: number) => setBookingsPage(page)
      }}
      renderItem={}
    />
  ) : null;
};
In the  renderItem  prop of the  <List />  in  userBookingsList , the difference we’ll make here from the list shown in the  <UserListings />  is that we’ll now display booking history above the listing card. In the  renderItem  prop function, we’ll create this  bookingHistory  element within a constant that will simply show the  checkIn  and  checkOut  values and we’ll render it above the  <ListingCard />  shown for each list item.
// ...
export const UserBookings = ({
  userBookings,
  bookingsPage,
  limit,
  setBookingsPage
}: Props) => {
  const total = userBookings ? userBookings.total : null;
  const result = userBookings ? userBookings.result : null;
  const userBookingsList = userBookings ? (
    <List
      grid={{
        gutter: 8,
        xs: 1,
        sm: 2,
        lg: 4
      }}
      dataSource={result ? result : undefined}
      locale={{ emptyText: "You haven't made any bookings!" }}
      pagination={{
        position: "top",
        current: bookingsPage,
        total: total ? total : undefined,
        defaultPageSize: limit,
        hideOnSinglePage: true,
        showLessItems: true,
        onChange: (page: number) => setBookingsPage(page)
      }}
      renderItem={userBooking => {
        const bookingHistory = (
          <div className="user-bookings__booking-history">
            <div>
              Check in: <Text strong>{userBooking.checkIn}</Text>
            </div>
            <div>
              Check out: <Text strong>{userBooking.checkOut}</Text>
            </div>
          </div>
        );
        return (
          <List.Item>
            {bookingHistory}
            <ListingCard listing={userBooking.listing} />
          </List.Item>
        );
      }}
    />
  ) : null;
};
Instead of returning the  userBookingsList  element in the return statement of  <UserBookings />  directly, we’ll have a conditionally rendered element called  userBookingsElement  that will be shown if the  userBookings  prop is available. By creating and returning this  userBookingsElement  and with the all the changes we’ve made, our  src/sections/User/components/UserBookings/index.tsx  file will look like the following:
client/src/sections/User/components/UserBookings/index.tsx
import React from "react";
import { List, Typography } from "antd";
import { ListingCard } from "../../../../lib/components";
import { User } from "../../../../lib/graphql/queries/User/__generated__/User";
interface Props {
  userBookings: User["user"]["bookings"];
  bookingsPage: number;
  limit: number;
  setBookingsPage: (page: number) => void;
}
const { Paragraph, Text, Title } = Typography;
export const UserBookings = ({
  userBookings,
  bookingsPage,
  limit,
  setBookingsPage
}: Props) => {
  const total = userBookings ? userBookings.total : null;
  const result = userBookings ? userBookings.result : null;
  const userBookingsList = userBookings ? (
    <List
      grid={{
        gutter: 8,
        xs: 1,
        sm: 2,
        lg: 4
      }}
      dataSource={result ? result : undefined}
      locale={{ emptyText: "You haven't made any bookings!" }}
      pagination={{
        position: "top",
        current: bookingsPage,
        total: total ? total : undefined,
        defaultPageSize: limit,
        hideOnSinglePage: true,
        showLessItems: true,
        onChange: (page: number) => setBookingsPage(page)
      }}
      renderItem={userBooking => {
        const bookingHistory = (
          <div className="user-bookings__booking-history">
            <div>
              Check in: <Text strong>{userBooking.checkIn}</Text>
            </div>
            <div>
              Check out: <Text strong>{userBooking.checkOut}</Text>
            </div>
          </div>
        );
        return (
          <List.Item>
            {bookingHistory}
            <ListingCard listing={userBooking.listing} />
          </List.Item>
        );
      }}
    />
  ) : null;
  const userBookingsElement = userBookingsList ? (
    <div className="user-bookings">
      <Title level={4} className="user-bookings__title">
        Bookings
      </Title>
      <Paragraph className="user-bookings__description">
        This section highlights the bookings you've made, and the check-in/check-out dates
        associated with said bookings.
      </Paragraph>
      {userBookingsList}
    </div>
  ) : null;
  return userBookingsElement;
};
And that’s it! We’ve created the  <UserListings />  and  <UserBookings />  components. In the  <User />  component file, let’s import these newly created components and save our changes.
client/src/sections/User/index.tsx
import { UserBookings, UserListings, UserProfile } from "./components";
At this moment in time, the user you’ve signed in with won’t have any listings or bookings data. To verify that at least the  <UserListings />  component works as intended, we’ll grab an id of a user from our mock  users  collection in the database and visit the user page directly based on the route.
/user/5d378db94e84753160e08b56 # example path for user in mock data that has listings
And… we see an error!
Why is this error being shown?  There are a few things we’ve missed on implementing on the server with regards to how the  listings  and  bookings  fields in our GraphQL API are to be resolved.
LISTINGRESOLVERS | BOOKINGRESOLVERS  - SERVER UPDATE
At this moment, we’ll switch over to briefly working on our Node server project.
In the server project, we’ve defined resolver functions for the  user  object. The  listings  and  bookings  field in our user object are to resolve to the  Listing  and  Booking  GraphQL object types.  We haven’t defined any resolver functions for the  Listing  and  Booking  GraphQL objects . Certain trivial resolver functions are handled for us by Apollo Server, however, we’ll need to set up explicit resolver functions for a few fields.
listingResolvers
We’ll begin by creating resolver functions for the  Listing  GraphQL object. We’ll create a  Listing/  folder that is to have an  index.ts  file within the  src/graphql/resolvers/  folder.
server/
  src/
    resolvers/
      // ...
      Listing/
        index.ts
      // ...
    // ..
  // ...
In the  src/graphql/resolvers/Listing/index.ts  file, we’ll create the  listingResolvers  map for the  Listing  GraphQL object type.
import { IResolvers } from "apollo-server-express";
export const listingResolvers: IResolvers = {};
Within  listingResolvers , we’ll specify the  id()  resolver function that will simply return the value of the  _id  field of the listing object in string format.
server/src/graphql/resolvers/Listing/index.ts
import { IResolvers } from "apollo-server-express";
import { Listing } from "../../../lib/types";
export const listingResolvers: IResolvers = {
  Listing: {
    id: (listing: Listing): string => {
      return listing._id.toString();
    }
  }
};
Note: A listing document in our database differs from a user document in our database since
_idfor a listing document is anObjectIdand not a string. This is why we’ve used thetoString()helper to convert the_idvalue to a string as we resolve theidfield for aListingGraphQL object.
There will be other resolvers we’ll eventually need to create for the  Listing  object but for the fields we’re attempting to access for the  /user/:id  page in our client, the  id()  resolver is the only one we’ll explicitly need to create. The other fields being queried for the  Listing  object in the  /user/:id  are being handled as trivial resolvers.
In the  src/graphql/resolvers/index.ts  file, we’ll import the  listingResolvers  map and place it in the Lodash  merge()  function
import merge from "lodash.merge";
import { listingResolver } from "./Listing";
import { userResolver } from "./User";
import { viewerResolver } from "./Viewer";
export const resolvers = merge(listingResolver, userResolver, viewerResolver);
bookingResolvers
We’ll also need to create a  bookingResolvers  map and some resolver functions for a few fields we query for in the  Booking  object in the  /user/:id  route.
In  src/graphql/resolvers/ , we’ll create a  Booking/  folder that is to have an  index.ts  file.
server/
  src/
    resolvers/
      // ...
      Booking/
        index.ts
      // ...
    // ..
  // ...
In the  src/graphql/resolvers/Booking/index.ts  file, we’ll create a  bookingsResolver  map. We’ll define a  Booking  object and specify the  id()  resolver function similar to how we’ve done in the  listingResolvers  map.
import { IResolvers } from "apollo-server-express";
import { Booking } from "../../../lib/types";
export const bookingResolvers: IResolvers = {
  Booking: {
    id: (booking: Booking): string => {
      return booking._id.toString();
    }
  }
};
There’s one other field we’re querying for in our client, for the  Booking  GraphQL object, that we’ll need an explicit resolver function for as well. In the  /user/:id  page, we’re querying for the  listing  field from the booking object expecting a listing object that summarizes the listing details of a booking. In the  booking  document in our database, we store  listing  as an  id  value but in the client we expect a listing object.
With that said, 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 value of the listing  _id  is equivalent to the id value of the  booking.listing  field.
server/src/graphql/resolvers/Booking/index.ts
import { IResolvers } from "apollo-server-express";
import { Database, Listing, Booking } from "../../../lib/types";
export const bookingResolvers: IResolvers = {
  Booking: {
    id: (booking: Booking): string => {
      return booking._id.toString();
    },
    listing: (
      booking: Booking,
      _args: {},
      { db }: { db: Database }
    ): Promise<Listing | null> => {
      return db.listings.findOne({ _id: booking.listing });
    }
  }
};
We’ll now import the  bookingResolvers  map in the  src/resolvers/index.ts  file, and place it in the Lodash merge() function.
server/src/graphql/resolvers/index.ts
import merge from "lodash.merge";
import { bookingResolvers } from "./Booking";
import { listingResolvers } from "./Listing";
import { userResolvers } from "./User";
import { viewerResolvers } from "./Viewer";
export const resolvers = merge(
  bookingResolvers,
  listingResolvers,
  userResolvers,
  viewerResolvers
);
When we save our changes in the server and visit the same user page we attempted to visit before in the client application, we’ll be presented with some listing cards for each listing that this user has.
<LISTINGCARD />
We’ll switch back to working on our React client project.
There are a few more improvements we’ll make before we discuss a few interesting points and close this lesson.
- All pricing information stored in our database is in cents . We’ll want to display pricing information for listings in the listing cards in dollar format.
- For the icon shown in the  <ListingCard />, we’ll like to show a nice blue color that matches the primary color being used in our application.
We’ll prepare a function to format currency and a constant to represent the icon color we’ll want to show in our app in the  src/lib/utils/index.ts  file where we keep shared app functions.
We’ll first create a  formatListingPrice()  function that’ll take two arguments:
- 
pricewhich is a number
- and a  roundboolean which we’ll default totrue.
// ...
export const formatListingPrice = (price: number, round = true) => {};
// ...
In most areas of our app, we’ll want to format the currency to dollars. We’ll also usually want price values rounded to a whole number since it’ll more presentable that way which is why we’ll have  round  as a function argument to control this.
The  formatListingPrice()  function will simply return a string containing the dollar symbol and a  formatListingPrice  value which will get the dollar value and be rounded depending on the value of the  round  argument.
client/src/lib/utils/index.ts
export const formatListingPrice = (price: number, round = true) => {
  const formattedListingPrice = round ? Math.round(price / 100) : price / 100;
  return `${formattedListingPrice}`;
};
We’ll also export an  iconColor  value which will simply be a string to represent the primary hex color of our app.
client/src/lib/utils/index.ts
export const iconColor = "#1890ff";
With these changes, our  src/lib/utils/index.ts  file will look like the following:
client/src/lib/utils/index.ts
import { message, notification } from "antd";
export const iconColor = "#1890ff";
export const formatListingPrice = (price: number, round = true) => {
  const formattedListingPrice = round ? Math.round(price / 100) : price / 100;
  return `${formattedListingPrice}`;
};
export const displaySuccessNotification = (
  message: string,
  description?: string
) => {
  return notification["success"]({
    message,
    description,
    placement: "topLeft",
    style: {
      marginTop: 50
    }
  });
};
export const displayErrorMessage = (error: string) => {
  return message.error(error);
};
In our  <ListingCard  /> component file, we’ll import the  iconColor  and  formatListingPrice()  function from the  src/lib/utils/index.ts  file.
client/src/lib/components/ListingCard/index.tsx
import { iconColor, formatListingPrice } from "../../utils";
We’ll make the following changes to the  <ListingCard />  component.
- We’ll use the  formatListingPrice()function to show the price in the<ListingCard />template. By not passing in a second argument, it’ll default to show a rounded value.
- We’ll apply a style to the  <Icon />element to specify the color to be theiconColorvalue we’ve set up in thesrc/lib/utils/index.tsfile.
- We want listing cards to be links to the listing page of a particular listing. To make this happen, we’ll import the  <Link />component fromreact-router-dom, we’ll destruct theidof a listing from thelistingprop object, and we’ll wrap our<ListingCard />component return statement with the<Link />component with a target route of/listing/${id}.
With these changes made to the  <ListingCard />  component, the  src/lib/components/ListingCard/index.tsx  file will look like the following.
client/src/lib/components/ListingCard/index.tsx
import React from "react";
import { Link } from "react-router-dom";
import { Card, Icon, Typography } from "antd";
import { iconColor, formatListingPrice } from "../../utils";
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 { id, title, image, address, price, numOfGuests } = listing;
  return (
    <Link to={`/listing/${id}`}>
      <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">
              {formatListingPrice(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" style={{ color: iconColor }} />
            <Text>{numOfGuests} guests</Text>
          </div>
        </div>
      </Card>
    </Link>
  );
};
When we now take a look at the same user page we’ve viewed before in our client application, we can see that the pricing details now show the dollar symbol. The price of each listing is in dollars per day. In addition, the listing cards are now links that when clicked will take the user to that  /listing/:id  page.
PAGINATION
Changing variables in  useQuery  Hook
We can see the pagination element shown when a user has more than 4 listings in total. If we were to click another page, our user page is loaded again and we’ll see another page of listings! Amazing!
If the page is loading and we see a new set of listings, this probably means we’re made another query, and we can confirm this from our browser’s network tab. With that said, why is another query for the  user  happening when we switch pages?
When we take a look at the use of the  useQuery  Hook in the  <User />  component, we’ve come to understand the  useQuery  Hook makes the request when the component mounts for the first time. When we click a new page button, the only thing that’s changed is the value of the  listingsPage  or  bookingsPage  state.
The  useQuery  Hook in React Apollo is smart enough to make  another  query request when the variables of the query  changes , and this occurs by default . This is especially helpful because our component is prepared to consume the  data ,  loading , and  error  properties of our query result anytime the query is made again.
When our query is in flight again,  loading  in the  <User />  component is set to true and our page shows the loading skeleton. When the new query is complete - we see the new list of listings.
Apollo Client Cache
There’s another important note we should talk about. If we head back to the client and try to navigate to a listing page we’ve already visited, our UI updates to show the listings that intend to be shown but a request isn’t being made again .
How is this working? This is due to something we’ve briefly mentioned before but now can witness. Apollo Client doesn’t only give us useful methods to conduct data fetching but sets up an in-memory intelligent cache without any configuration on our part .
When we make requests to retrieve data with Apollo Client, Apollo Client under the hood caches the data . The next time we return to the page that we’ve just visited, Apollo 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 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.
Apollo Client also gives us the capability to directly update information in the cache when needed.
Are there ways we can tell Apollo Client to force the request from the network and not from the cache? Yes, and we’ll see examples of this later in the course.
Okay. We’re going to stop here for now. Great job so far! The UI we’ve built for the  /user/:id  page will be very similar to the other pages we intend to build in our client application.
MODULE 6 SUMMARY
In this module, we had the client be able to request and present information for a certain user in the  /user/:id  route of our application.
SERVER PROJECT
SRC/GRAPHQL/TYPEDEFS.TS
We created a single root-level  user  field that can be queried from the client to receive a user’s information. The  user  query field queries for a user in the  "users"  collection of our database based on the  id  argument provided.
server/src/graphql/typeDefs.ts
  type Query {
    authUrl: String!
    user(id: ID!): User!
  }
The  User  object returned from the  user  query field is to have certain information about a user we want the client to access.
server/src/graphql/typeDefs.ts
  type User {
    id: ID!
    name: String!
    avatar: String!
    contact: String!
    hasWallet: Boolean!
    income: Int
    bookings(limit: Int!, page: Int!): Bookings
    listings(limit: Int!, page: Int!): Listings!
  }
SRC/GRAPHQL/RESOLVERS/USER/INDEX.TS
The  userResolvers  map in the  src/graphql/resolvers/User/index.ts  file contains the resolver functions that pertain to the  User  object/domain.
The root-level query  user()  resolver function simply looks to find the user from the  "users"  collection from the  id  argument provided. If the viewer making the request  is  the user being requested, we add an  authorized  property to the  user  object to constitute that the viewer is authorized to see certain information about the  user .
server/src/graphql/resolvers/User/index.ts
export const userResolvers: IResolvers = {  Query: {
    user: async (
      _root: undefined,
      { id }: UserArgs,
      { db, req }: { db: Database; req: Request }
    ): Promise<User> => {
      try {
        const user = await db.users.findOne({ _id: id });
        if (!user) {
          throw new Error("user can't be found");
        }
        const viewer = await authorize(db, req);
        if (viewer && viewer._id === user._id) {
          user.authorized = true;
        }
        return user;
      } catch (error) {
        throw new Error(`Failed to query user: ${error}`);
      }
    }
  },};
We declare a few other additional resolver functions for the  User  object. The  id()  resolver simply returns the  _id  value of a user document. The  hasWallet()  resolver returns the presence of the  walletId  field of a user document. The  income  field returns the  user.income  only if the viewer is authorized to see this information.
server/src/graphql/resolvers/User/index.ts
export const userResolvers: IResolvers = {    id: (user: User): string => {
      return user._id;
    },
    hasWallet: (user: User): boolean => {
      return Boolean(user.walletId);
    },
    income: (user: User): number | null => {
      return user.authorized ? user.income : null;
    },};
bookings()  and  listings()  are resolver functions that help return a paginated list of bookings and listings respectively. The  bookings()  resolver only returns data if the viewer is authorized.
server/src/graphql/resolvers/User/index.ts
export const userResolvers: IResolvers = {    bookings: async (
      user: User,
      { limit, page }: UserBookingsArgs,
      { db }: { db: Database }
    ): Promise<UserBookingsData | null> => {
      try {
        if (!user.authorized) {
          return null;
        }
        const data: UserBookingsData = {
          total: 0,
          result: []
        };
        let cursor = await db.bookings.find({
          _id: { $in: user.bookings }
        });
        cursor = cursor.skip(page > 0 ? (page - 1) * limit : 0);
        cursor = cursor.limit(limit);
        data.total = await cursor.count();
        data.result = await cursor.toArray();
        return data;
      } catch (error) {
        throw new Error(`Failed to query user bookings: ${error}`);
      }
    },};
server/src/graphql/resolvers/User/index.ts
export const userResolvers: IResolvers = {    listings: async (
      user: User,
      { limit, page }: UserListingsArgs,
      { db }: { db: Database }
    ): Promise<UserListingsData | null> => {
      try {
        const data: UserListingsData = {
          total: 0,
          result: []
        };
        let cursor = await db.listings.find({
          _id: { $in: user.listings }
        });
        cursor = cursor.skip(page > 0 ? (page - 1) * limit : 0);
        cursor = cursor.limit(limit);
        data.total = await cursor.count();
        data.result = await cursor.toArray();
        return data;
      } catch (error) {
        throw new Error(`Failed to query user listings: ${error}`);
      }
    }};
SRC/GRAPHQL/RESOLVERS/BOOKING/INDEX.TS
We’ve created explicit resolver functions for the  id  and  listing  field in a  Booking  object to have these fields resolve to their expected values when queried from the client.
server/src/graphql/resolvers/Booking/index.ts
export const bookingResolvers: IResolvers = {
  Booking: {
    id: (booking: Booking): string => {
      return booking._id.toString();
    },
    listing: (
      booking: Booking,
      _args: {},
      { db }: { db: Database }
    ): Promise<Listing | null> => {
      return db.listings.findOne({ _id: booking.listing });
    }
  }
};
SRC/GRAPHQL/RESOLVERS/LISTING/INDEX.TS
We’ve also created an explicit resolver function for the  id  field of a  Listing  object to have it resolved to its expected value when queried from the client.
server/src/graphql/resolvers/Listing/index.ts
export const listingResolvers: IResolvers = {
  Listing: {
    id: (listing: Listing): string => {
      return listing._id.toString();
    }
  }
};
CLIENT PROJECT
SRC/LIB/GRAPHQL/QUERIES/USER/INDEX.TS
In the client, we constructed the  User  GraphQL document in the  src/lib/graphql/queries/User/index.ts  file.
client/src/lib/graphql/queries/User/index.ts
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
        }
      }
    }
  }
`;
SRC/SECTIONS/USER/INDEX.TSX
In the  <User />  component rendered in the  /user/:id  route, we construct the entire user page that involves but is not limited to:
- Making the  userquery when the component first mounts.
- Presenting the user profile information in the  <UserProfile />child component.
- Presenting the list of listings the user owns in the  <UserListings />child component.
- Presenting the list of bookings the user has made in the  <UserBookings />child component.
Appropriate loading and error state UI is also presented when the  user  query is in the loading or error state. When data is available from the query, the relevant user information is shown to the user.
client/src/sections/User/index.tsx
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>
  );
};
MOVING FORWARD
In the next module, we begin building the server and client implementation that will help allow us to retrieve and display information for listings in the  /listing/:id  route of our application.
MODULE 7 INTRODUCTION
In this module, we’ll look to have listing information be queried from the database and shown in the Listing page of our application. The Listing page is where a user can see details about a certain listing and where the user will eventually be able to book that listing for a period of time.
The Listing page is to be displayed in the  /listing/:id  route of our app and in its complete state will look similar to the following:
In this module, we’ll:
- Update our GraphQL API to query information for a specific listing from our database.
- Build the UI of the Listing page in our client project.
LISTING GRAPHQL FIELDS
In the last module, we managed to build the server GraphQL fields and client representation of a user. In this module, we’ll look to build the server and client portions of getting information for a single listing . To display information about our listings in our client app, we will need some kind of functionality to query and resolve listing data on our server.
Just like how we have a single root-level field to query for a user, we’ll have a single root-level field to query for a certain listing. The one thing we’ll keep in mind with our  listing  query field is that we should only return the sensitive portions of the listing data if the user is requesting their own listing. The sensitive data we’re referring to is the bookings made for a certain listing. Only the user querying for their own listing page should be able to access the bookings made to their listing. We’ll implement this authorization in the coming lessons.
Let’s first prepare the  listing  GraphQL field and the accompanying resolver function. We’ll first define the  listings  field in the root  Query  object of our GraphQL API and we’ll say, for now, that its expected return type is a defined string value.
server/src/graphql/typeDefs.ts
  type Query {
    authUrl: String!
    user(id: ID!): User!
    listing: String!
  }
Next, we’ll construct the resolver for the  listing  query field. When we built the user module, we defined the structure of a  Listing  GraphQL object since listings can be queried within a user. As a result, we also had to define the explicit resolver function for the  id  field for the  Listing  object, so we created the  listingResolvers  map to contain the resolvers for the listing module.
In the  listingResolvers  map within the  src/graphql/resolvers/Listing/index.ts  file, we’ll now create the resolver for the root level  listing  field from the  Query  root object and we’ll say, at this moment, it is to return a string that is to say  Query.listing .
server/src/graphql/resolvers/Listing/index.ts
import { IResolvers } from "apollo-server-express";
import { Listing } from "../../../lib/types";
export const listingResolvers: IResolvers = {
  Query: {
    listing: () => {
      return "Query.listing";
    }
  },
  Listing: {
    id: (listing: Listing): string => {
      return listing._id.toString();
    }
  }
};
With our server project running, if we head over to the GraphQL Playground at http://localhost:9000/api and execute the  listing  query, we’ll see the  "Query.listing"  output. In the next lesson, we’ll build the functionality for the  listing()  resolver to actually query for listing information from the database.
BUILDING THE LISTING RESOLVERS
With the root-level  listing  field prepared in our GraphQL API, we’ll construct the resolver function for this field to attempt to query for the appropriate listing from the listings collection in our database. Similar to how the  user  query field queried for a user from our database with a certain ID, the  listing  query field will query for a certain listing based on the ID provided.
As a result, we’ll update the  listing  field in our GraphQL type definitions and state it expects a defined argument of  id  of type GraphQL ID. In addition, the  listing  field when resolved should return the appropriate  Listing  GraphQL object.
server/src/graphql/typeDefs.ts
  type Query {
    authUrl: String!
    user(id: ID!): User!
    listing(id: ID!): Listing!
  }
This  Listing  GraphQL object has been created in the last module and has fields to describe the certain listing - such as it’s  title ,  description ,  host , etc.
LISTING()
We’ll now modify the resolver function for the  listing  field to state that it is to accept an  id  input from our client and return a  Listing  object when resolved. First, we’ll construct the interface of the expected arguments for this field in a  types.ts  file kept within the  src/graphql/resolvers/Listing  folder. We’ll create a  ListingArgs  interface that is to have an  id  field of type  string .
server/src/graphql/resolvers/Listing/types.ts
export interface ListingArgs {
  id: string;
}
In our  listingResolvers  map within the  src/graphql/resolvers/Listing/index.ts  file, we’ll import a few things we’ll need for our  listing()  resolver function. We’ll first import the  Database  and  Listing  interfaces that have been defined in the  src/lib/types.ts  file. We’ll also import the recently created  ListingArgs  interface from the  types.ts  file adjacent to this file.
// ...
import { Database, Listing } from "../../../lib/types";
import { ListingArgs } from "./types";
We’ll now update the  listing()  resolver function to query for a certain listing from the database.
- We’ll define the  listing()resolver function as anasyncfunction.
- In the  listing()resolver function, we’ll access theidargument passed in and thedbobject available in the context of our resolver.
- When the  listing()resolver function is to be complete, it should return aPromisethat when resolved is an object of typeListing.
// ...
export const listingResolvers: IResolvers = {
  Query: {
    listing: async (
      _root: undefined,
      { id }: ListingArgs,
      { db, req }: { db: Database; req: Request }
    ): Promise<Listing> => {}
  },
  Listing: {
    // ...
  }
};
The  listing()  resolver function will be fairly straightforward to implement. We’ll use Mongo’s  findOne()  method to find a listing document from the  listings  collection where the  _id  field is the  ObjectId  representation of the  id  argument passed in. If this listing document doesn’t exist, we’ll throw a new  Error . If the listing document does exist, we’ll return the  listing  document that’s been found. We’ll have this implementation be kept in a  try  block while in a  catch  statement - we’ll catch an error if ever to arise and have it thrown within a new error message.
// ...
import { ObjectId } from "mongodb";
// ...
export const listingResolvers: IResolvers = {
  Query: {
    listing: async (
      _root: undefined,
      { id }: ListingArgs,
      { db, req }: { db: Database; req: Request }
    ): Promise<Listing> => {
      try {
        const listing = await db.listings.findOne({ _id: new ObjectId(id) });
        if (!listing) {
          throw new Error("listing can't be found");
        }
        return listing;
      } catch (error) {
        throw new Error(`Failed to query listing: ${error}`);
      }
    }
  },
  Listing: {
    // ...
  }
};
LISTING()  AUTHORIZE
The  listing  object contains a series of fields where we’ll need to define explicit resolver functions for a certain number of them. In the last lesson, we mentioned that the  bookings  field within a listing object should be authorized and shown only to the user who owns the listing. When we define the resolver for the listing  booking  field, we’ll need to check if the listing query is authorized.
We’ll follow a similar format to what we did for the  User  module and simply get the viewer details with the  authorize()  function available in the  src/lib/utils/  folder. Within the  listing()  resolver function, we’ll have an  if  statement to check if the viewer id matches that of the listing  host  field which will determine the viewer is querying for their own listing. If this is  true , we’ll set an  authorized  field in the  listing  object to be  true .
With that said, the first thing we’ll do is add the  authorized  field to the  Listing  TypeScript interface in the  src/lib/types.ts  file and state that it is to be of type  boolean  when defined.
server/src/lib/types.ts
export interface Listing {
  _id: ObjectId;
  title: string;
  description: string;
  image: string;
  host: string;
  type: ListingType;
  address: string;
  country: string;
  admin: string;
  city: string;
  bookings: ObjectId[];
  bookingsIndex: BookingsIndexYear;
  price: number;
  numOfGuests: number;
  authorized?: boolean;
}
In our  listingResolvers  map file, we’ll import the  authorize()  function from the  src/lib/utils/  folder. We’ll also import the  Request  interface from  express .
In the  listing()  resolver function, we’ll access the  req  object available as part of context in all our resolvers. Within the function, we’ll have the  authorize()  function be run and pass in the  db  and  req  objects it expects, and we’ll do this after the listing document has already been found. With the  viewer  obtained from the  authorize()  function, we’ll then state that if  viewer._id  matches the  listing.host  field, we’ll set the  authorized  value of the  listing  object to  true .
With these changes, the  listing()  resolver function will be finalized as follows:
server/src/graphql/resolvers/Listing/index.ts
export const listingResolvers: IResolvers = {  Query: {
    listing: async (
      _root: undefined,
      { id }: ListingArgs,
      { db, req }: { db: Database; req: Request }
    ): Promise<Listing> => {
      try {
        const listing = await db.listings.findOne({ _id: new ObjectId(id) });
        if (!listing) {
          throw new Error("listing can't be found");
        }
        const viewer = await authorize(db, req);
        if (viewer && viewer._id === listing.host) {
          listing.authorized = true;
        }
        return listing;
      } catch (error) {
        throw new Error(`Failed to query listing: ${error}`);
      }
    }
  },};
Note: The
hostfield within thelistingdocument object is anidof the host of the listing (i.e. the user who owns the listing).
We’ll now create explicit resolver functions for the fields in the  Listing  object that we want to be resolved differently than the value being kept in the database. We already have the  id()  resolver set-up to resolve the  _id  of the listing document to an  id  string representation when queried from the client.
HOST()
The  host  field in a  listing  document in the database is a string ID of the user that owns the listing. When the client queries for this field, we’ll want the client to receive object information of the  host . Since we want to resolve the host field to a  User  object value, we’ll import the  User  interface from the  src/lib/types.ts  file.
We’ll define a resolver function for the  host()  field in the  Listing  object within our  listingResolvers  map, and we’ll use MongoDB’s  findOne()  method to find the host from the  listing.host  id value.
// ...
import { Database, Listing, User } from "../../../lib/types";
// ...
export const listingResolvers: IResolvers = {
  Query: {
    // ...
  },
  Listing: {
    // ...,
    host: async (
      listing: Listing,
      _args: {},
      { db }: { db: Database }
    ): Promise<User> => {
      const host = await db.users.findOne({ _id: listing.host });
      if (!host) {
        throw new Error("host can't be found");
      }
      return host;
    }
  }
};
Since there’s only one request that can fail in the  host()  resolver function, we won’t use a  try...catch  block and throw the only error of not finding the host if that error is to occur.
Note : As a reminder, the
_idfield for theuserdocument in our Mongo database is of typestringand not of typeObjectID. MongoDB natively creates anObjectIDtype for the_idfields but we’ve resorted to having the user’s_idfield be astringsince we simply capture whateveridGoogle OAuth returns to us. Thehostin a listing document is the samestringrepresentation of this ID.
BOOKINGSINDEX()
In one of our earlier modules, we highlighted how the dates of bookings that have been made to listings are captured in a  bookingsIndex  field within a listing document. A listing  bookingsIndex  field is to be a key/value representation of the dates that have already been booked. On the client-side, we’ll want this key/value object returned since on the client we’ll look to control which dates a user can book on the listing page. For example, if a booking has been made on a certain date, we’ll want to prevent a user from booking that same date.
We’ll want  listing.bookingsIndex  returned to the client as an object but unfortunately, the  bookingsIndex  object within a listing document is an unstructured data set where we won’t know what the values are going to be so we won’t be able to define the GraphQL type of  bookingsIndex . As a result, we’ve defined the  bookingsIndex  field in our  Listing  GraphQL object  as a string . We’ll create a resolver for the  bookingsIndex  field in the  Listing  object that simply stringifies the  bookingsIndex  object within a listing document to a string.
// ...
export const listingResolvers: IResolvers = {
  Query: {
    // ...
  },
  Listing: {
    // ...,
    bookingsIndex: (listing: Listing): string => {
      return JSON.stringify(listing.bookingsIndex);
    }
  }
};
On the client, we’ll receive the  bookingsIndex  of a listing as a string and we’ll parse it to get the object we’re looking for.
BOOKINGS()
Finally, we’ll create the resolver function for the  bookings  field that is to be a paginated list of bookings that have been made for a certain listing. The structure of how we create this resolver will be almost identical to the paginated  bookings  field in the  User  object.
- It’ll be offset-based pagination where the field accepts  limitandpagearguments and will return data that contains thetotalnumber of bookings returned and aresultarray of the list within a certain page.
- We’ll want the  bookingsfield within alistingobject to be authorized only for a viewer viewing their own listing.
- We’ll use Mongo DB’s  cursorcapability to find the paginated list of document objects and the total amount.
We’ll first define the TypeScript interfaces for the arguments the  booking()  resolver function is to receive and the data it’s expected to return. We’ll define these types in the  types.ts  file within the  src/graphql/resolver/Listing/types.ts  file. We’ll import the  Booking  interface (that describes the shape of a booking document in the database) from the  src/lib/types.ts  file. We’ll define the  ListingBookingArgs  interface which is to have  limit  and  page  properties of type  number . We’ll define a  ListingBookingData  interface that is to have a  total  field of type  number  and a  result  field which is to be an array of type  Booking .
With all the changes we’ve made, the  types.ts  file within the  src/graphql/resolvers/Listing/  folder will appear as follows:
server/src/graphql/resolvers/Listing/types.ts
import { Booking } from "../../../lib/types";
export interface ListingArgs {
  id: string;
}
export interface ListingBookingsArgs {
  limit: number;
  page: number;
}
export interface ListingBookingsData {
  total: number;
  result: Booking[];
}
We’ll import the newly created  ListingBookingArgs  and  ListingBookingData  interfaces in the  listingResolvers  map file. We’ll look to copy over the  bookings()  resolver function from the  userResolvers  map and change the context of a few certain things:
- We’ll say the root object passed in is  listingand is of typeListing.
- The shape of arguments passed in to the resolver function is  ListingBookingArgs.
- The function, when resolved successfully, should return a  Promisethat when resolved will be an object of shapeListingBookingDataornull.
- In the resolver function, we’ll check for the  authorizedfield from thelistingobject.
- The data constructed within the function will be of type  ListingBookingData.
- The  $inoperator used within the MongoDBfind()method will reference thelisting.bookingsarray.
- Finally, in the  catchstatement, if an error was to ever occur we’ll fire an error message of"Failed to query listing bookings".
With this implemented, the  bookings()  resolver function will appear as the following:
// ...
import { ListingArgs, ListingBookingsArgs, ListingBookingsData } from "./types";
export const listingResolvers: IResolvers = {
  Query: {
    // ...
  },
  Listing: {
    // ...,
    bookings: async (
      listing: Listing,
      { limit, page }: ListingBookingsArgs,
      { db }: { db: Database }
    ): Promise<ListingBookingsData | null> => {
      try {
        if (!listing.authorized) {
          return null;
        }
        const data: ListingBookingsData = {
          total: 0,
          result: []
        };
        let cursor = await db.bookings.find({
          _id: { $in: listing.bookings }
        });
        cursor = cursor.skip(page > 0 ? (page - 1) * limit : 0);
        cursor = cursor.limit(limit);
        data.total = await cursor.count();
        data.result = await cursor.toArray();
        return data;
      } catch (error) {
        throw new Error(`Failed to query listing bookings: ${error}`);
      }
    }
  }
};
And with all the changes we’ve made in this lesson, the  src/graphql/resolvers/Listing/index.ts  file will appear as follows:
server/src/graphql/resolvers/Listing/index.ts
import { IResolvers } from "apollo-server-express";
import { Request } from "express";
import { ObjectId } from "mongodb";
import { Database, Listing, User } from "../../../lib/types";
import { authorize } from "../../../lib/utils";
import { ListingArgs, ListingBookingsArgs, ListingBookingsData } from "./types";
export const listingResolvers: IResolvers = {
  Query: {
    listing: async (
      _root: undefined,
      { id }: ListingArgs,
      { db, req }: { db: Database; req: Request }
    ): Promise<Listing> => {
      try {
        const listing = await db.listings.findOne({ _id: new ObjectId(id) });
        if (!listing) {
          throw new Error("listing can't be found");
        }
        const viewer = await authorize(db, req);
        if (viewer && viewer._id === listing.host) {
          listing.authorized = true;
        }
        return listing;
      } catch (error) {
        throw new Error(`Failed to query listing: ${error}`);
      }
    }
  },
  Listing: {
    id: (listing: Listing): string => {
      return listing._id.toString();
    },
    host: async (
      listing: Listing,
      _args: {},
      { db }: { db: Database }
    ): Promise<User> => {
      const host = await db.users.findOne({ _id: listing.host });
      if (!host) {
        throw new Error("host can't be found");
      }
      return host;
    },
    bookingsIndex: (listing: Listing): string => {
      return JSON.stringify(listing.bookingsIndex);
    },
    bookings: async (
      listing: Listing,
      { limit, page }: ListingBookingsArgs,
      { db }: { db: Database }
    ): Promise<ListingBookingsData | null> => {
      try {
        if (!listing.authorized) {
          return null;
        }
        const data: ListingBookingsData = {
          total: 0,
          result: []
        };
        let cursor = await db.bookings.find({
          _id: { $in: listing.bookings }
        });
        cursor = cursor.skip(page > 0 ? (page - 1) * limit : 0);
        cursor = cursor.limit(limit);
        data.total = await cursor.count();
        data.result = await cursor.toArray();
        return data;
      } catch (error) {
        throw new Error(`Failed to query listing bookings: ${error}`);
      }
    }
  }
};
LISTINGTYPE  - ERROR
That’s it for now! All the functions we’ve defined for the  Listing  object -  bookings() ,  bookingsIndex() ,  host() , and  id()  are explicit resolver functions for how we want these fields to be returned. The other fields within the  Listing  object are to be trivially resolved such as the  title ,  description ,  image , etc.
If we were to query the fields for a certain listing in our database, we should get the data resolved from our API. Let’s give this a check. With our server project running, we’ll first head over to GraphQL Playground. To query for a certain listing, we need to pass in the ID argument of that listing. We’ll grab the string id of a certain listing from our MongoDB Atlas dashboard. We’ll pass that value for the  id  argument in our query and we’ll look to query for all the fields in the  listing  object. This would look something like this:
query {
  listing(id: "5d378db94e84753160e08b30") {
    id
    title
    description
    image
    host {
      id
    }
    type
    address
    city
    bookings(limit: 4, page: 1) {
      total
    }
    bookingsIndex
    price
    numOfGuests
  }
}
When we run our query, we see an error.
The error message says  "Expected a value of type \"ListingType\" but received: \"apartment\""  and comes from the  listing.type  field.
In our GraphQL API schema, we’ve defined the GraphQL  ListingType  Enum as  APARTMENT  and  HOUSE  in capital letters.
server/src/graphql/typeDefs.ts
  enum ListingType {
    APARTMENT
    HOUSE
  }
In our TypeScript definition, we define the values of our  ListingType  Enum as  "apartment"  and  "house"  but in lower-case letters.
export enum ListingType {
  Apartment = "apartment",
  House = "house"
}
Enums, in GraphQL, can behave differently depending on the GraphQL server implementation however in our instance - the GraphQL Enum structure is to be mapped to the TypeScript Enum values. In essence, we have an issue where the capital letter format isn’t being matched to the lower-case format being used to seed our database . The error comes from the fact that GraphQL states that the returned data from the database doesn’t match the GraphQL schema contract.
There are two simple ways we can resolve this issue.
- Have the values in our TypeScript Enum definition be in capital letters and re-seed our database.
- Change the GraphQL Enum definition and state the  apartmentandhouseproperties in lower-case format.
Some folks within the GraphQL community often state that Enums in GraphQL schemas should be defined in capital letters as best practice. We’ll stick with this practice and attempt to have the values in our TypeScript Enum definition in capital letters.
This will require us to update the values of the  ListingType  interface in the  src/lib/types.ts  to  "APARTMENT"  and  "HOUSE" .
server/src/lib/types.ts
export enum ListingType {
  Apartment = "APARTMENT",
  House = "HOUSE"
}
We’ll then clear our database with the  clear  script.
npm run clear
And re-seed the database with the  seed  script to ensure all the type fields in our listing documents are of the capital letter format.
npm run seed
If we now head back to GraphQL Playground and re-run our query for the root-level  listing  field, our query now resolves successfully!
In the next lesson, we’ll begin building the client UI for the  /listing/:id  page.








