Overview
In this section, we'll cover:
- set up the Apollo Client in our React app
- use Hooks to fetch data from our supergraph to get a list of Products and view the details of a Product
- how to optimise queries using deferred responses
Prerequisites
- Our supergraph running in the cloud (or in the local machine using
rover dev
)
Front-end
Now that we've built and deployed the subgraphs, 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 projectwebsite
- 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:
cd websitenpm install
To start a local copy of the app on port 3000:
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 GraphQL queries in KBT Threads React App, we'll be using Apollo Client.
Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL.
Let's configure Apollo Client and set the Router URL to ensure our client is making calls to our newly deployed router.
Open website/src/index.js
. Import Apollo Client with additional modules: Apollo Provider, and InMemoryCache:
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
Lastly, we need to instantiate Apollo Client with the Router URI, instantiate the localized cache, and configure a name to inform the GraphOS management plane during metrics capturing.
Note: If you forgot your Router's URI, you can run the command: gcloud run services describe <yourname>-router --region us-east1 --format 'value(status.url)'
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 GraphQL queries.
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 GraphOS Explorer to construct a getFeaturedProducts
query on the supergraph:
To make our edits, open website/src/pages/Homepage.js
in your code editor.
- Add
gql
anduseQuery
imports from@apollo/client
at the top of the file, as follows:
import { gql, useQuery } from "@apollo/client";
- Next, we create a
GET_FEATURED_PRODUCTS
query usinggql
. As illustrated in Studio Explorer, thisgetFeaturedProducts
query takes in a limit argument that specifies how many products to return.
export const GET_FEATURED_PRODUCTS = gql`query HomePageFeaturedProducts($limit: Int) {getFeaturedProducts(limit: $limit) {idnamepricedescriptionimagesshortDescription}}`;
- Now that we've created the query, we can
useQuery
(literally!) to make a GraphQL 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 variable set by business needs.
export default function HomePage() {const { error, loading, data } = useQuery(GET_FEATURED_PRODUCTS, {variables: { limit: 10 },});// rest of the file}
- Underneath our
useQuery
code, we can include logic to account for error messages, when the call is unsuccessful.
if (error) return <Error error={error} />;
- Otherwise, we will display those products on the Homepage using
SimpleGrid
andProductCard
components.
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>);
Here is a screenshot of our 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 query could look like using Studio Explorer:
Set up your variables with the following payload:
{"productId": "113"}
Enter product ID 113
, and copy the query to the operation box.
query GetProductDetails($productId: ID!) {product(id: $productId) {idnamedescriptionpriceimages}}
Now we have an idea what the query looks like, let's bring that into the React code.
Navigate to website/src/pages/Product.js
file and create a GET_PRODUCT_DETAILS
query as shown below:
export const GET_PRODUCT_DETAILS = gql`query GetProductDetails($productId: ID!) {product(id: $productId) {idnamedescriptionpriceimagesvariants {colorwaysizeinStockid}}}`;
This example also uses the gql
and useQuery
we imported from @apollo/client
. The code below does the following:
- Makes the request using
useQuery
- Returns an error message if the request was unsuccessful
- Parses out a successful request and displays the product details
- Calls our initialization function after completion of the response
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:
[Optional] Optimize latency for Product details page with @defer
While our page is complete, we can optimize the experience by using a client-side directive called defer
.
Defer allows us to split up the query 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 resolvers, or non-critical path operations, etc.
Within our Product
subgraph, the variant resolvers make a sequential database call for the variant after querying 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 GraphOS Explorer by using our pre-existing query, and wrap the variants
field with the @defer
directive.
query GetProductDetails($productId: ID!) {product(id: $productId) {idnamedescriptionpriceimagesvariants {colorwaysizeinStockid}}}
Copy over the query to Explorer, select and highlight the variants portion of the query. Right click and select Wrap with inline @defer Fragment.
Let's execute the query and see what happens:
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 variants 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 query.
export const GET_PRODUCT_DETAILS = gql`query GetProductDetails($productId: ID!) {product(id: $productId) {idnamedescriptionpriceimages... @defer {variants {colorwaysizeinStockid}}}}`;
At last, we've learned how to use the defer directive 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 directive in a query, 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 subgraph: Products. It's time to leverage the power of the new supergraph and make a query that spans multiple subgraphs.
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 subgraphs:
Users
subgraph for user information (e.g first and last name, email, address, etc)Orders
subgraph for user's previous ordersProducts
subgraph for products in user's active cart
Using Studio Explorer, let's experiment some more with the KBT Threads supergraph to see how we would build a query 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 fields.
Feel free to try that out yourself to build the following query, or you can copy and paste this to save time:
query GetAccountSummary($userId: ID!) {user(id: $userId) {firstNamelastNameaddressactiveCart {items {idcolorwaysizepriceparent {idnameimages}}subtotal}orders {iditems {idsizecolorwaypriceparent {idnameimages}}}}}
In the Variables panel, set userId
to 10
, as follows:
{"userId": "10"}
The screenshots below illustrate the query/operation we want to make and the Query Plan preview. The Query Plan is a visual way to reason how the GraphOS Router will execute the query across the 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 query 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 query plan.
Here, the query plan shows that Router 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
- [Fetch Product] - Products API to get details based on Item ID (
activeCart.items[ colorway, size, price, parent]
) - [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 Router to provide the necessary inputs.
- [Fetch Product] - Products API to get details based on Item ID (
- [Fetch Orders] - Orders API to get Item IDs
- [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
query as shown below:
export const GET_ACCOUNT_SUMMARY = gql`query getAccountSummary($userId: ID!) {user(id: $userId) {firstNamelastNameaddressactiveCart {items {idcolorwaysizepriceparent {idnameimages}}subtotal}orders {iditems {idsizecolorwaypriceparent {idnameimages}}}}}`;
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.
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:
🎉 Congratulations! Your front-end is ready! 🎉
Up next
Now that we have a working supergraph 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 graph