Overview
Now that we've initialized Apollo Client and taken care of generating our types, we can give our client its first query to execute.
In this lesson, we will:
- Provide our React component with a GraphQL query to execute
- Handle loading, error, and render states for our response data
📦 A query in a component
The code for our tracks page lives in src/pages/tracks.tsx. At the moment, the page just displays the bare layout that we've seen previously. Let's add a query definition to it.
Just like when we defined our schema, we need to wrap all GraphQL strings in the gql function. Let's import gql:
import { gql } from "../__generated__/";
Next we'll declare a constant called GET_TRACKS with an empty GraphQL string (by convention, query constants are in ALL_CAPS):
const GET_TRACKS = gql(`# Query goes here`);
Note: We use the gql import from our __generated__/index.ts file as a function, with parentheses wrapping the backticks and operation!
Now, remember the query we built in the Apollo Explorer to retrieve track data? Conveniently, that's exactly the query we need!
Head back to the Explorer, where we'll access the query from our Sandbox operation collection.
When we click on TracksForHome from our collection, the saved query is automatically inserted into a new tab in the Operation panel.
Let's copy the query, and return to our code.
We can now paste the query directly into our empty gql function.
/** GET_TRACKS query to retrieve all tracks */const GET_TRACKS = gql(`query GetTracks {tracksForHome {idtitlethumbnaillengthmodulesCountauthor {idnamephoto}}}`);
Now that our frontend code contains an actual GraphQL operation, we can run our npm run generate function again and let the GraphQL Code Generator scan and anticipate the operations that our app will be sending. It will use this information to determine the TypeScript types for our operations. Run the generate command:
npm run generate
Great! Now our generated types understand what kind of query we're going to send, and what kind of data we expect to get back.
Our query is ready to execute. Let's finally display some catstronauts on our homepage!
📡 Executing with useQuery
To execute queries, we'll use Apollo Client's useQuery hook.
The useQuery hook takes in GraphQL query string as an argument.
When our component renders, useQuery returns an object that contains loading, error, and data properties that we can use to render our UI. Let's put all of that into code.
Note: Check out the official Apollo docs on the useQuery hook to learn more about this function.
First, we need to import useQuery from the @apollo/client package :
import { gql } from "../__generated__";import { useQuery } from "@apollo/client";
Now, in our Tracks functional component (below the opened curly brace), we'll declare three destructured constants from our useQuery hook: loading, error, and data. We call useQuery with our GET_TRACKS query as its argument:
const { loading, error, data } = useQuery(GET_TRACKS);
Below that, we'll first use the loading constant:
if (loading) return "Loading...";
As long as loading is true (indicating the query is still in flight), the component will just render a Loading... message.
When loading is false, the query is complete. This means we either have data, or we have an error.
Let's add another conditional statement that handles the error state:
if (error) return `Error! ${error.message}`;
If we don't have an error, we must have data! For now, we'll just dump our raw data object with JSON.stringify to see what happens.
<Layout grid>{JSON.stringify(data)}</Layout>
With all of that added, here's what the completed Tracks component looks like. Make sure yours matches!
const Tracks = () => {const { loading, error, data } = useQuery(GET_TRACKS);if (loading) return "Loading...";if (error) return `Error! ${error.message}`;return <Layout grid>{JSON.stringify(data)}</Layout>;};
Let's restart our app. We first see the loading message, then a raw JSON response. The response includes a tracksForHome object (the name of our operation), which contains an array of Track objects. Looks good so far! Now, let's use this data in an actual view.
Rendering TrackCards
Conveniently, we already have a TrackCard component that's ready to go. We'll need to import the component and feed the response data to it, but first let's open /src/containers/track-card.tsx to see how it works.
/*** Track Card component renders basic info in a card format* for each track populating the tracks grid homepage.*/const TrackCard: React.FC<{ track: any }> = ({ track }) => {const { title, thumbnail, author, length, modulesCount, id } = track;//...};
Right away we can see that the TrackCard component accepts a prop called track, but right now its type is any. Now that we've generated types from our GraphQL server, we can fix this and more accurately describe the type of data the track prop should provide.
At the top of the file, let's import the Track type that exists in our __generated__ folder's graphql file.
import type { Track } from '../__generated__/graphql'
We can use this Track type to set the track prop's data type, replacing any.
/*** Track Card component renders basic info in a card format* for each track populating the tracks grid homepage.*/const TrackCard: React.FC<{ track: Track }> = ({ track }) => {const { title, thumbnail, author, length, modulesCount, id } = track;//...};
Now if you hover over the Track type we just added, you'll see the breakdown of the exact type that we defined in our backend schema. We know exactly the details that are available for us to use on data of this type! Let's break them down.
The component takes a track prop and uses its title, thumbnail, author, length, modulesCount, and id. So, we just need to pass each TrackCard a Track object from our query response.
Let's head back to src/pages/tracks.tsx. We've seen that the server response to our GET_TRACKS GraphQL query includes a tracksForHome key, which contains the array of tracks.
First, let's import the TrackCard component.
import TrackCard from "../containers/track-card";
To create one card per track, we'll map through the tracksForHome array and return a TrackCard component with its corresponding track data as its prop:
<Layout grid>{data?.tracksForHome?.map((track) => (<TrackCard key={track.id} track={track} />))}</Layout>
Right away, we'll see that there's an error on the track property!
For context, let's take a look at the Track type that the GraphQL Code Generator generated for us.
/** A track is a group of Modules that teaches about a specific topic */export type Track = {__typename?: 'Track';/** The track's main Author */author: Author;/** The track's complete description, can be in markdown format */description?: Maybe<Scalars['String']['output']>;id: Scalars['ID']['output'];/** The track's approximate length to complete, in minutes */length?: Maybe<Scalars['Int']['output']>;/** The track's complete array of Modules */modules: Array<Module>;/** The number of modules this track contains */modulesCount?: Maybe<Scalars['Int']['output']>;/** The number of times a track has been viewed */numberOfViews?: Maybe<Scalars['Int']['output']>;/** The track's illustration to display in track card or track page detail */thumbnail?: Maybe<Scalars['String']['output']>;/** The track's title */title: Scalars['String']['output'];};
Here we can see that modules is not an optional property on our Track type.
Because the query we're making inside of this component does not include modules
details, TypeScript is letting us know that we're violating one of the rules of this
Track type.
To fix this, we'll jump back into containers/track-card.tsx. Here we'll update the type signature of the TrackCard to omit modules from the properties it requires on the Track type we pass it.
const TrackCard: React.FC<{ track: Omit<Track, "modules"> }> = ({ track }) => {const { title, thumbnail, author, length, modulesCount, id } = track;// ... TrackCard body}
This allows us to pass the track property an object that mostly adheres to the Track TypeScript type—just without its modules!
We refresh our browser, and voila! We get a bunch of nice-looking cards with cool catstronaut thumbnails. Our track title, length, number of modules, and author information all display nicely thanks to our TrackCard component. Pretty neat!
Wrapping query results
While refreshing the browser, you might have noticed that because we return the loading message as a simple string, we don't currently show the component's entire layout and navbar while it's loading (the same issue goes for the error message). We should make sure that our UI's behavior is consistent throughout all of a query's phases.
That's where our QueryResult helper component comes in. This isn't a component that's provided directly by an Apollo library. We've added it to use query results in a consistent, predictable way throughout our app.
Let's open components/query-result. This component takes the useQuery hook's return values as props. It then performs basic conditional logic to either render a spinner, an error message, or its children:
const QueryResult: React.FC<PropsWithChildren<QueryResultProps>> = ({loading,error,data,children,}): React.ReactElement<any, any> | null => {if (error) {return <p>ERROR: {error.message}</p>;}if (loading) {return (<SpinnerContainer><LoadingSpinner data-testid="spinner" size="large" theme="grayscale" /></SpinnerContainer>);}if (data) {return <>{children}</>;}return <p>Nothing to show...</p>;};
Back to our tracks.tsx file, we'll import QueryResult at the top:
import QueryResult from "../components/query-result";
We can now remove the lines in this file that handle the loading and error states, because the QueryResult component will handle them instead.
const Tracks = () => {const { loading, error, data } = useQuery(GET_TRACKS);- if (loading) return "Loading...";- if (error) return `Error! ${error.message}`;return (<Layout grid>{data?.tracksForHome?.map((track) => (<TrackCard key={track.id} track={track} />))}</Layout>);};
We wrap QueryResult around our map function and give it the props it needs:
return (<Layout grid><QueryResult error={error} loading={loading} data={data}>{data?.tracksForHome?.map((track) => (<TrackCard key={track.id} track={track} />))}</QueryResult></Layout>);
Refreshing our browser, we get a nice spinner while loading, and then our cards appear!
After all that code, the tracks.tsx file should look like this:
And there you have it! Our homepage is populated with a cool grid of track cards, as laid out in our initial mock-up.
Practice
Create a ListSpaceCats query with a spaceCats query field and its name, age and missions selection set. For the missions field, select name and description
Use the useQuery hook with the SPACECATS query and destructure the loading, error and data properties from the result.
useQuery hook used for?Key takeaways
- The
useQueryhook is the primary API for executing queries in a React application. - The
useQueryhook returns an object that containsloading,error, anddataproperties that we can use to determine the elements in our UI.
Up next
Our homepage looks good, but we've got nowhere to go from here: up next, let's explore how we can set up our app to show details for just one track object.
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.