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 TrackCard
s
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
useQuery
hook is the primary API for executing queries in a React application. - The
useQuery
hook returns an object that containsloading
,error
, anddata
properties 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.