/
Launch Apollo Studio

Fetch data with queries

Working with the useQuery React Hook


Time to accomplish: 20 Minutes

Now that we've set up Apollo Client, we can integrate it into our React app. This lets us use React Hooks to bind the results of GraphQL queries directly to our UI.

The code blocks below use TypeScript by default. You can use the dropdown menu above each code block to switch to JavaScript.

If you're using JavaScript, use .js and .jsx file extensions wherever .ts and .tsx appear.

Integrate with React

To connect Apollo Client to React, we wrap our app in the ApolloProvider component from the @apollo/client package. We pass our client instance to the ApolloProvider component via the client prop.

Open src/index.tsx and replace its contents with the following:

src/index.tsx
import {
  ApolloClient,
  NormalizedCacheObject,
  ApolloProvider
} from '@apollo/client';
import { cache } from './cache';
import React from 'react';
import ReactDOM from 'react-dom';
import Pages from './pages';
import injectStyles from './styles';

// Initialize ApolloClient
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
  cache,
  uri: 'http://localhost:4000/graphql',
});

injectStyles();

// Pass the ApolloClient instance to the ApolloProvider component
ReactDOM.render(
  <ApolloProvider client={client}>
    <Pages />
  </ApolloProvider>,
  document.getElementById('root')
);

The ApolloProvider component is similar to React’s context provider: it wraps your React app and places client on the context, which enables you to access it from anywhere in your component tree.

Now we're ready to build React components that execute GraphQL queries.

Display a list of launches

Let's build the page in our app that shows a list of available SpaceX launches. Open src/pages/launches.tsx. Right now, the file looks like this:

src/pages/launches.tsx
import React, { Fragment, useState }  from 'react';
import { RouteComponentProps } from '@reach/router';
import { gql } from '@apollo/client'

export const LAUNCH_TILE_DATA = gql`
  fragment LaunchTile on Launch {
    __typename
    id
    isBooked
    rocket {
      id
      name
    }
    mission {
      name
      missionPatch
    }
  }
`;

interface LaunchesProps extends RouteComponentProps {}

const Launches: React.FC<LaunchesProps> = () => {
  return <div />;
}

export default Launches;

Define the query

First, we'll define the shape of the query we'll use to fetch a paginated list of launches. Paste the following below the declaration of LAUNCH_TILE_DATA:

src/pages/launches.tsx
export const GET_LAUNCHES = gql`
  query GetLaunchList($after: String) {
    launches(after: $after) {
      cursor
      hasMore
      launches {
        ...LaunchTile
      }
    }
  }
  ${LAUNCH_TILE_DATA}
`;

Using fragments

Notice that our query definition pulls in the LAUNCH_TILE_DATA definition above it. LAUNCH_TILE_DATA defines a GraphQL fragment, which is named LaunchTile. A fragment is useful for defining a set of fields that you can include across multiple queries without rewriting them.

In the query above, we include the LaunchTile fragment in our query by preceding it with ..., similar to JavaScript spread syntax.

Pagination details

Notice that in addition to fetching a list of launches, our query fetches hasMore and cursor fields. That's because the launches query returns paginated results:

  • The hasMore field indicates whether there are additional launches beyond the list returned by the server.
  • The cursor field indicates the client's current position within the list of launches. We can execute the query again and provide our most recent cursor as the value of the $after variable to fetch the next set of launches in the list.

Apply the useQuery hook

We'll use Apollo Client's useQuery React Hook to execute our new query within the Launches component. The hook's result object provides properties that help us populate and render our component throughout the query's execution.

  1. Modify your @apollo/client import to include useQuery, and import a few predefined components for rendering the page:

    src/pages/launches.tsx
    import { gql, useQuery } from '@apollo/client';
    import { LaunchTile, Header, Button, Loading } from '../components';

    If you're using TypeScript, also import the necessary types that are generated from your server's schema definitions:

    src/pages/launches.tsx
    import * as GetLaunchListTypes from './__generated__/GetLaunchList';
  2. Replace the dummy declaration of const Launches with the following:

    src/pages/launches.tsx
    const Launches: React.FC<LaunchesProps> = () => {
      const {
        data,
        loading,
        error
      } = useQuery<
        GetLaunchListTypes.GetLaunchList,
        GetLaunchListTypes.GetLaunchListVariables
      >(GET_LAUNCHES);
    
      if (loading) return <Loading />;
      if (error) return <p>ERROR</p>;
      if (!data) return <p>Not found</p>;
    
      return (
        <Fragment>
          <Header />
          {data.launches &&
            data.launches.launches &&
            data.launches.launches.map((launch: any) => (
              <LaunchTile key={launch.id} launch={launch} />
            ))}
        </Fragment>
      );
    }

This component passes our GET_LAUNCHES query to useQuery and obtains data, loading, and error properties from the result. Depending on the state of those properties, we render a list of launches, a loading indicator, or an error message.

Start up both your server and client with npm start and visit localhost:3000. If everything's configured correctly, our app's main page appears and lists 20 SpaceX launches!

We have a problem though: there are more than 20 SpaceX launches in total. Our server paginates its results and includes a maximum of 20 launches in a single response.

To be able to fetch and store all launches, we need to modify our code to use the cursor and hasMore fields included in our query. Let's learn how.

Add pagination support

Apollo Client 3 provides new pagination helper functions for offset-based and Relay-style pagination that are not yet reflected in this tutorial.

Apollo Client provides a fetchMore helper function to assist with paginated queries. It enables you to execute the same query with different values for variables (such as the current cursor).

Add fetchMore to the list of objects we destructure from the useQuery result object, and also define an isLoadingMore state variable:

src/pages/launches.tsx
const Launches: React.FC<LaunchesProps> = () => {
  const {
    data,
    loading,
    error,
    fetchMore  } = useQuery<
    GetLaunchListTypes.GetLaunchList,
    GetLaunchListTypes.GetLaunchListVariables
  >(GET_LAUNCHES);
  const [isLoadingMore, setIsLoadingMore] = useState(false);  // ...
}

Now we can connect fetchMore to a button within the Launches component that fetches additional launches when it's clicked.

Paste this code directly above the closing </Fragment> tag in the Launches component:

src/pages/launches.tsx
{data.launches && data.launches.hasMore && (
  isLoadingMore
    ? <Loading />
    : <Button
        onClick={async () => {
          setIsLoadingMore(true);
          await fetchMore({
            variables: {
              after: data.launches.cursor,
            },
          });
          setIsLoadingMore(false);
        }}
      >
        Load More
      </Button>
)}
//</Fragment>

When our new button is clicked, it calls fetchMore (passing the current cursor as the value of the after variable) and displays a Loading notice until the query returns results.

Let's test our button. Start everything up and visit localhost:3000 again. A Load More button now appears below our 20 launches. Click it. After the query returns, no additional launches appear. 🤔

If you check your browser's network activity, you'll see that the button did in fact send a followup query to the server, and the server did in fact respond with a list of launches. However, Apollo Client keeps these lists separate, because they represent the results of queries with different variable values (in this case, the value of after).

We need Apollo Client to instead merge the launches from our fetchMore query with the launches from our original query. Let's configure that behavior.

Merge cached results

Apollo Client stores your query results in its in-memory cache. The cache handles most operations intelligently and efficiently, but it doesn't automatically know that we want to merge our two distinct lists of launches. To fix this, we'll define a merge function for the paginated field in our schema.

Open src/cache.ts, where our default InMemoryCache is initialized:

src/cache.ts
import { InMemoryCache, Reference } from '@apollo/client';

export const cache: InMemoryCache = new InMemoryCache({});

The schema field that our server paginates is the list of launches. Modify the initialization of cache to add a merge function for the launches field, like so:

src/cache.ts
export const cache: InMemoryCache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        launches: {
          keyArgs: false,
          merge(existing, incoming) {
            let launches: Reference[] = [];
            if (existing && existing.launches) {
              launches = launches.concat(existing.launches);
            }
            if (incoming && incoming.launches) {
              launches = launches.concat(incoming.launches);
            }
            return {
              ...incoming,
              launches,
            };
          }
        }
      }
    }
  }
});

This merge function takes our existing cached launches and the incoming launches and combines them into a single list, which it then returns. The cache stores this combined list and returns it to all queries that use the launches field.

This example demonstrates a use of field policies, which are cache configuration options that are specific to individual fields in your schema.

If you try clicking the Load More button now, the UI will successfully append additional launches to the list!

Display a single launch's details

We want to be able to click a launch in our list to view its full details. Open src/pages/launch.tsx and replace its contents with the following:

src/pages/launch.tsx
import { gql } from '@apollo/client';
import { LAUNCH_TILE_DATA } from './launches';

export const GET_LAUNCH_DETAILS = gql`
  query LaunchDetails($launchId: ID!) {
    launch(id: $launchId) {
      site
      rocket {
        type
      }
      ...LaunchTile
    }
  }
  ${LAUNCH_TILE_DATA}
`;

This query includes all the details we need for the page. Notice that we're reusing the LAUNCH_TILE_DATA fragment that's already defined in launches.tsx.

Once again, we'll pass our query to the useQuery hook. This time, we also need to pass the corresponding launch's launchId to the query as a variable. The value of launchId is available as a prop that's passed by the router.

Now replace the contents of launch.tsx with the following:

src/pages/launch.tsx
import React, { Fragment } from 'react';
import { gql, useQuery } from '@apollo/client';

import { LAUNCH_TILE_DATA } from './launches';
import { Loading, Header, LaunchDetail } from '../components';
import { ActionButton } from '../containers';
import { RouteComponentProps } from '@reach/router';
import * as LaunchDetailsTypes from './__generated__/LaunchDetails';

export const GET_LAUNCH_DETAILS = gql`
  query LaunchDetails($launchId: ID!) {
    launch(id: $launchId) {
      site
      rocket {
        type
      }
      ...LaunchTile
    }
  }
  ${LAUNCH_TILE_DATA}
`;

interface LaunchProps extends RouteComponentProps {
  launchId?: any;
}

const Launch: React.FC<LaunchProps> = ({ launchId }) => {
  const {
    data,
    loading,
    error,
  } = useQuery<
    LaunchDetailsTypes.LaunchDetails,
    LaunchDetailsTypes.LaunchDetailsVariables
  >(GET_LAUNCH_DETAILS,
    { variables: { launchId } }
  );

  if (loading) return <Loading />;
  if (error) return <p>ERROR: {error.message}</p>;
  if (!data) return <p>Not found</p>;

  return (
    <Fragment>
      <Header image={data.launch && data.launch.mission && data.launch.mission.missionPatch}>
        {data && data.launch && data.launch.mission && data.launch.mission.name}
      </Header>
      <LaunchDetail {...data.launch} />
      <ActionButton {...data.launch} />
    </Fragment>
  );
}

export default Launch;

Just like before, we use the status of the query to render either a loading or error state, or data when the query completes.

Return to your app and click a launch in the list to view its details page.

Display the profile page

We want a user's profile page to display a list of launches that they've booked a seat on. Open src/pages/profile.tsx and replace its contents with the following:

src/pages/profile.tsx
import React, { Fragment } from 'react';
import { gql, useQuery } from '@apollo/client';

import { Loading, Header, LaunchTile } from '../components';
import { LAUNCH_TILE_DATA } from './launches';
import { RouteComponentProps } from '@reach/router';
import * as GetMyTripsTypes from './__generated__/GetMyTrips';

export const GET_MY_TRIPS = gql`
  query GetMyTrips {
    me {
      id
      email
      trips {
        ...LaunchTile
      }
    }
  }
  ${LAUNCH_TILE_DATA}
`;

interface ProfileProps extends RouteComponentProps {}

const Profile: React.FC<ProfileProps> = () => {
  const {
    data,
    loading,
    error
  } = useQuery<GetMyTripsTypes.GetMyTrips>(
    GET_MY_TRIPS,
    { fetchPolicy: "network-only" }  );
  if (loading) return <Loading />;
  if (error) return <p>ERROR: {error.message}</p>;
  if (data === undefined) return <p>ERROR</p>;

  return (
    <Fragment>
      <Header>My Trips</Header>
      {data.me && data.me.trips.length ? (
        data.me.trips.map((launch: any) => (
          <LaunchTile key={launch.id} launch={launch} />
        ))
      ) : (
        <p>You haven't booked any trips</p>
      )}
    </Fragment>
  );
}

export default Profile;

You should recognize all of the concepts in this code from the pages we've already completed, with one highlighted exception: we're setting a fetchPolicy.

Customizing the fetch policy

As mentioned earlier, Apollo Client stores query results in its cache. If you query for data that's already present in your cache, Apollo Client can return that data without needing to fetch it over the network.

However, cached data can become stale. Slightly stale data is acceptable in many cases, but we definitely want our user's list of booked trips to be up to date. To handle this, we've specified a fetch policy for our GET_MY_TRIPS query.

A fetch policy defines how Apollo Client uses the cache for a particular query. The default policy is cache-first, which means Apollo Client checks the cache to see if the result is present before making a network request. If the result is present, no network request occurs.

By setting this query's fetch policy to network-only, we guarantee that Apollo Client always queries our server to fetch the user's most up-to-date list of booked trips.

For a list of all supported fetch policies, see Supported fetch policies.

If you visit the profile page in your app, you'll notice that the query returns null. This is because we still need to implement login functionality. We'll tackle that in the next section!

Edit on GitHub