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 React hook is the primary API for executing queries in an Apollo 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.

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.

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!

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 page, we'll import QueryResult:

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. Here's the same code snippet in the context of the live app.

Note: The example above is hooked up to live data. Your local copy will still be showing mock data.

How do we execute queries in our front-end app?

And there you have it! Our homepage is populated with a cool grid of track cards, as laid out in our initial mock-up.

Previous
Next