10. The useQuery hook
5m

📡 Executing with useQuery

Time to execute our TRACKS query from React! To do that, we'll use Apollo Client's useQuery hook in src/pages/tracks.js.

The useQuery hook is the primary API for executing queries in a React application. We run a query within a React component by calling useQuery and passing it our GraphQL query string. This makes running queries from React components a breeze.

When our component renders, useQuery returns an object from Apollo Client 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 (we're already importing gql):

import {useQuery, gql} 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 TRACKS query as its argument:

const {loading, error, data} = useQuery(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 it!

const Tracks = () => {
const {loading, error, data} = useQuery(TRACKS);
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
return <Layout grid>{JSON.stringify(data)}</Layout>;
};

Code Challenge!

Use the useQuery hook with the SPACECATS query

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.

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:

import TrackCard from '../containers/track-card';

Let's open /src/containers/track-card.js 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 = ({track}) => {
const {title, thumbnail, author, length, modulesCount} = track;
//...
};

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 query response.

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

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>

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

Note: You might see a warning in the browser console saying something like, "Encountered two children with the same key, track_01." This is happening because we're still mocking our track data, so every track has the same id, but React wants each key to be unique. This warning will go away after we update our server to use real track data (in Lift-off II), so we can safely ignore it for now.

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 (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 = ({loading, error, data, children}) => {
if (error) {
return <p>ERROR: {error.message}</p>;
}
if (loading) {
return (
<SpinnerContainer>
<LoadingSpinner data-testid="spinner" size="large" theme="grayscale" />
</SpinnerContainer>
);
}
if (!data) {
return <p>Nothing to show...</p>;
}
if (data) {
return children;
}
};

Back to our tracks.js 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.

We wrap QueryResult around our map function and give it the props it needs:

<QueryResult error={error} loading={loading} data={data}>
{data?.tracksForHome?.map(track => (
<TrackCard key={track.id} track={track} />
))}
</QueryResult>

Refreshing our browser, we get a nice spinner while loading, and then our cards appear!

What is the useQuery hook used for?

After all that code, the tracks.js file should look like this:

import React from 'react';
import {useQuery, gql} from '@apollo/client';
import TrackCard from '../containers/track-card';
import {Layout, QueryResult} from '../components';
/** TRACKS gql query to retrieve all tracks */
const TRACKS = gql`
query GetTracks {
tracksForHome {
id
title
thumbnail
length
modulesCount
author {
name
photo
}
}
}
`;
/**
* Tracks Page is the Catstronauts home page.
* We display a grid of tracks fetched with useQuery with the TRACKS query
*/
const Tracks = () => {
const {loading, error, data} = useQuery(TRACKS);
return (
<Layout grid>
<QueryResult error={error} loading={loading} data={data}>
{data?.tracksForHome?.map((track, index) => (
<TrackCard key={track.id} track={track} />
))}
</QueryResult>
</Layout>
);
};
export default Tracks;

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!

🏆 Course complete!

Congrats on completing the first feature of our Catstronauts app! 🚀

To build our homepage grid feature, we used a schema-first approach, meaning we considered data needs from the client's perspective before even starting to code.

We defined our schema and used Apollo Server to build a basic GraphQL endpoint that provides mocked responses.

We then used the Apollo Sandbox Explorer to interactively build and test queries against our local GraphQL server.

Finally, we developed the client side of our Catstronauts app. We used React, Apollo Client, and the useQuery hook to perform a query on our GraphQL server and to display our tracks in a nice grid card layout.

In the following course, we'll connect our app to live data using a REST data source and write our first resolvers to provide that data to clients.

Previous