4. Building our front-end
10m

Overview

In this section, we'll cover:

  • set up the Apollo Client in our React app
  • use Hooks to fetch data from our to get a list of Products and view the details of a Product
  • how to optimise queries using deferred responses

Prerequisites

  • Our running in the cloud (or in the local machine using rover dev)

Front-end

Now that we've built and deployed the , it's time to see how it all comes together on a front-end application.

In the cloned repository, there are two folders for the front-end application:

  • final/website - contains the completed front-end project
  • website - is the folder where you can follow along to complete this part of the workshop

Let's set up the website, and install all dependencies:

In your workshop root folder
cd website
npm install

To start a local copy of the app on port 3000:

website/
npm start

Our front-end application will be available on https://localhost:3000, changes made will reflect the site.

Setting up Apollo Client

In order to make our queries in KBT Threads React App, we'll be using .

is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with .

Let's configure and set the URL to ensure our client is making calls to our newly deployed router.

Open website/src/index.js. Import with additional modules: Apollo Provider, and InMemoryCache:

website/src/index.js
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";

Lastly, we need to instantiate with the URI, instantiate the localized cache, and configure a name to inform the management plane during metrics capturing.

Note: If you forgot your 's URI, you can run the command: gcloud run services describe <yourname>-router --region us-east1 --format 'value(status.url)'

website/src/index.js
const client = new ApolloClient({
uri: "YOUR_ROUTER_URI",
cache: new InMemoryCache(),
name: "web-workshop-client",
version: "0.1",
});

For context, we are also wrapping our application with a React Provider that will pass along the client context to our components when we make our queries.

website/src/index.js
ReactDOM.render(
<ChakraProvider theme={theme}>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</ChakraProvider>,
document.getElementById("root")
);

✏️ Getting featured Products on the homepage

KBT Threads would like to feature 10 products for sale on the home page.

First, let's use Explorer to construct a getFeaturedProducts on the :

https://studio.apollographql.com

Building the Top 10 Query

To make our edits, open website/src/pages/Homepage.js in your code editor.

  1. Add gql and useQuery imports from @apollo/client at the top of the file, as follows:
website/src/pages/Homepage.js
import { gql, useQuery } from "@apollo/client";
  1. Next, we create a GET_FEATURED_PRODUCTS using gql. As illustrated in Studio Explorer, this getFeaturedProducts takes in a limit that specifies how many products to return.
website/src/pages/Homepage.js
export const GET_FEATURED_PRODUCTS = gql`
query HomePageFeaturedProducts($limit: Int) {
getFeaturedProducts(limit: $limit) {
id
name
price
description
images
shortDescription
}
}
`;
  1. Now that we've created the , we can useQuery (literally!) to make a request for the featured products. In the figure below, we've hardcoded the limit to 10, so the API returns 10 featured products. In future optimizations, this could be a dynamic set by business needs.
website/src/pages/Homepage.js
export default function HomePage() {
const { error, loading, data } = useQuery(GET_FEATURED_PRODUCTS, {
variables: { limit: 10 },
});
// rest of the file
}
  1. Underneath our useQuery code, we can include logic to account for error messages, when the call is unsuccessful.
website/src/pages/Homepage.js
if (error) return <Error error={error} />;
  1. Otherwise, we will display those products on the Homepage using SimpleGrid and ProductCard components.
website/src/pages/Homepage.js
return (
<Stack direction="column" spacing="12">
<VStack direction="column" spacing="2" py="10">
<Heading size="2xl">Welcome to KBT Threads</Heading>
<Text fontSize="2xl"> A thread for every occasion! </Text>
</VStack>
<Stack direction="column" spacing="4">
<Heading as="h2" size="lg">
Products
</Heading>
{loading ? (
<Spinner />
) : (
<SimpleGrid columns={[1, null, 2]} spacing={4}>
{data?.getFeaturedProducts.map((product) => (
<ProductCard key={product.id} {...product} />
))}
</SimpleGrid>
)}
</Stack>
</Stack>
);
Task!

Here is a screenshot of our Home page:

http://localhost:3000

Home Page

✏️ Getting Product details

KBT Threads would like to show a detailed product page whenever the user clicks on a featured product on the homepage. Let's make another GQL request for the product detail using the product ID passed from the homepage.

Before jumping to your code editor, you can experiment with what the product detail could look like using Studio Explorer:

https://studio.apollographql.com

Building the Product Query

Set up your with the following payload:

{
"productId": "113"
}

Enter product ID 113, and copy the to the box.

query GetProductDetails($productId: ID!) {
product(id: $productId) {
id
name
description
price
images
}
}

Now we have an idea what the looks like, let's bring that into the React code.

Navigate to website/src/pages/Product.js file and create a GET_PRODUCT_DETAILS as shown below:

website/src/pages/product.js
export const GET_PRODUCT_DETAILS = gql`
query GetProductDetails($productId: ID!) {
product(id: $productId) {
id
name
description
price
images
variants {
colorway
size
inStock
id
}
}
}
`;

This example also uses the gql and useQuery we imported from @apollo/client. The code below does the following:

  1. Makes the request using useQuery
  2. Returns an error message if the request was unsuccessful
  3. Parses out a successful request and displays the product details
  4. Calls our initialization function after completion of the response
website/src/pages/product.js
const { id } = useParams();
const response = useQuery(GET_PRODUCT_DETAILS, {
variables: { productId: id },
onCompleted: (data) => {
updatePage(data);
},
});
const { loading, error, data = {} } = response;
if (loading) return <Spinner />;
if (error) return <Error error={error.message} />;
const { name, description, images } = data?.product || {};

Once we've updated the code, we should have a final page:

http://localhost:3000/product/113

Product page screenshot

[Optional] Optimize latency for Product details page with @defer

While our page is complete, we can optimize the experience by using a client-side called defer.

Defer allows us to split up the into specified chunks, allowing the response to be streamed (multi-part) back to our application asynchronously. This is particularly useful for client-teams to maximize performance for things like: expensive payloads, cross-region , or non-critical path , etc.

Within our Product , the make a sequential database call for the variant after for the product. As you can imagine, this can potentially cause long response times for the client when requesting a product's variants, making it a great use-case for defer.

Let's visualize how this works in Explorer by using our pre-existing , and wrap the variants with the @defer .

query GetProductDetails($productId: ID!) {
product(id: $productId) {
id
name
description
price
images
variants {
colorway
size
inStock
id
}
}
}

Copy over the to Explorer, select and highlight the variants portion of the . Right click and select Wrap with inline @defer Fragment.

Studio - Inline with Defer

Let's execute the and see what happens:

Studio - Inline with Defer

Notice the new section within the response panel, Response Timeline. Here we can see each chunk being returned to the client against the overall timeline by hovering over the nodes.

When you hover over the first node, you can see the product details, and the second node will reveal all the of the product.

Now that we have an understanding of how defer can improve the client-side, let's apply this to our product detail page (website/src/pages/product.js), and update the .

website/src/pages/Product.js
export const GET_PRODUCT_DETAILS = gql`
query GetProductDetails($productId: ID!) {
product(id: $productId) {
id
name
description
price
images
... @defer {
variants {
colorway
size
inStock
id
}
}
}
}
`;

At last, we've learned how to use the defer to optimize the client-side allowing us to paint our page without being blocked by large queries.

One important note is that you are not limited to one defer in a , so you can strategically break apart large queries with multiple defers.

Learn: read more about the @defer here: https://www.apollographql.com/docs/graphos/routing/defer/

Querying multiple subgraphs

In the last two examples, we've only made requests to one : Products. It's time to leverage the power of the new and make a that spans multiple .

KBT Threads would like to add a new Orders page that shows user information, and information from active cart and previous orders. To accomplish this, we will need information from three different :

  • Users for user information (e.g first and last name, email, address, etc)
  • Orders for user's previous orders
  • Products for products in user's active cart

Using Studio Explorer, let's experiment some more with the KBT Threads to see how we would build a to satisfy this feature request.

Note: Client developers find it useful to leverage the Documentation Sidebar within Explorer to freely build queries in an interactive way, by clicking the "+" sign for the .

Feel free to try that out yourself to build the following , or you can copy and paste this to save time:

query GetAccountSummary($userId: ID!) {
user(id: $userId) {
firstName
lastName
email
address
activeCart {
items {
id
colorway
size
price
parent {
id
name
images
}
}
subtotal
}
orders {
id
items {
id
size
colorway
price
parent {
id
name
images
}
}
}
}
}

In the panel, set userId to 10, as follows:

{
"userId": "10"
}

The screenshots below illustrate the / we want to make and the Query Plan preview. The is a visual way to reason how the will execute the query across the .

https://studio.apollographql.com

Querying multiple subgraphs

[Bonus] Taking a closer look at the query plan

As we already know, a common problem front-end developers is managing the orchestration of retrieving data, especially in a REST environment.

If we tried to replicate this exact in a REST world, we would've had to figure out how to stitch these responses, coordinate their relationships, and verify the validity of the data all while referring to documentation that can be sparse or outdated.

In addition, we would typically abstract this orchestration into a SDK or Services module to hide the details on the client-side code. These are a lot of considerations that have historically bogged client-side development.

Instead, the GraphOS Router handles that orchestration logic with the .

https://studio.apollographql.com

Multiple subgraphs Query Plan

Here, the shows that will execute the following:

  • [Fetch Customer] - Customer API to get User Data, Cart Item IDs, and Order IDs (firstName, lastName, email, address, cart.items.ids, order.items.id)
  • Parallel Request
      1. [Fetch Product] - Products API to get details based on Item ID (activeCart.items[ colorway, size, price, parent])
      2. [Fetch Customer] - Provide the price for each item in the cart to customers API to determine the subtotal, which could be useful for things like taxes, shipping, and locality (activeCart.subtotal) Note: Customer only owns the business logic, where the schema informs the to provide the necessary inputs.
      1. [Fetch Orders] - Orders API to get Item IDs
      2. [Fetch Products] - Products API to determine product details (order.items[size, colorway, price, parent])

✏️ Implementing Account summary query on the Accounts page

Navigate to website/src/pages/Account.js file and create a GET_ACCOUNT_SUMMARY as shown below:

website/src/pages/Account.js
export const GET_ACCOUNT_SUMMARY = gql`
query getAccountSummary($userId: ID!) {
user(id: $userId) {
firstName
lastName
email
address
activeCart {
items {
id
colorway
size
price
parent {
id
name
images
}
}
subtotal
}
orders {
id
items {
id
size
colorway
price
parent {
id
name
images
}
}
}
}
}
`;

As we've done in previous examples, make the request using useQuery, handle if an error is returned, parse out a successful request and display the user order details as shown below.

website/src/pages/Account.js
const response = useQuery(GET_ACCOUNT_SUMMARY, {
variables: { userId: "10" },
});
const { loading, error, data = {} } = response;
if (loading) return <Spinner />;
if (error) return <Error error={error.message} />;
const { firstName, lastName, email, address, activeCart, orders } =
data.user || {};

After saving the file, the http://localhost:3000/account page should refresh and render a page like such:

http://localhost:3000/account

Account Page

Task!

🎉 Congratulations! Your front-end is ready! 🎉

Up next

Now that we have a working and a front-end built for KBT Threads, there is a desire to extend a subset of this functionality to additional external groups. In the next section, we'll see how to:

  • set up Contracts to restrict access to sensitive information in our
Previous