📡 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>;};
Use the useQuery
hook with the SPACECATS
query and destructure the loading
, error
and data
properties from the result.
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:
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!
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!
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 {idtitlethumbnaillengthmodulesCountauthor {namephoto}}}`;/*** 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.
🏆 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.
See you in the next mission, Lift-off II!
Share your questions and comments about this lesson
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.