6. Defining a query
2m

Overview

Now that we've initialized and taken care of generating our types, we can give our client its first to execute.

In this lesson, we will:

  • Provide our React component with a 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 definition to it.

Just like when we defined our schema, we need to wrap all strings in the gql function. Let's import gql:

src/pages/tracks.tsx
import { gql } from "../__generated__/";

Next we'll declare a constant called GET_TRACKS with an empty string (by convention, 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 !

Now, remember the 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 from our Sandbox collection.

http://studio.apollographql.com/sandbox/explorer

Opening the Operation Collections panel to access a saved operation.

When we click on TracksForHome from our collection, the saved is automatically inserted into a new tab in the Operation panel.

http://studio.apollographql.com/sandbox/explorer

Clicking on an operation saved in a collection to insert it into the Operation panel.

Let's copy the , and return to our code.

We can now paste the directly into our empty gql function.

/** GET_TRACKS query to retrieve all tracks */
const GET_TRACKS = gql(`
query GetTracks {
tracksForHome {
id
title
thumbnail
length
modulesCount
author {
id
name
photo
}
}
}
`);

Now that our frontend code contains an actual , we can run our npm run generate function again and let the Code Generator scan and anticipate the 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 we're going to send, and what kind of data we expect to get back.

Our is ready to execute. Let's finally display some catstronauts on our homepage!

📡 Executing with useQuery

To execute queries, we'll use 's useQuery hook.

The useQuery hook takes in string as an .

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 :

tracks.tsx
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 as its :

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 is still in flight), the component will just render a Loading... message.

When loading is false, the 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 ), which contains an array of Track objects. Looks good so far! Now, let's use this data in an actual view.

http://localhost:3000

The JSON response to our query, outputted directly into the UI

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 } = 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 , 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.

src/containers/track-card.tsx
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 } = 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, and modulesCount. So, we just need to pass each TrackCard a Track object from our response.

Let's head back to src/pages/tracks.tsx. We've seen that the server response to our GET_TRACKS includes a tracksForHome key, which contains the array of tracks.

First, let's import the TrackCard component.

pages/tracks.tsx
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 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 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 } = 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!

http://localhost:3000

The UI of our Catstronauts app, displaying a number of track cards

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 '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 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:

components/query-result.tsx
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!

http://localhost:3000

The UI of our Catstronauts app, displaying a number of track cards

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.

Task!

Practice

Code Challenge!

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

Loading...
Loading progress
Which of the following are best practices when creating client queries?
Code Challenge!

Use the useQuery hook with the SPACECATS query and destructure the loading, error and data properties from the result.

What is the 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 contains loading, error, and data 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.

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.

You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.