The Problem with REST: Over-fetching and Under-fetching

With a traditional REST API, the server defines the shape of the data you get back from an endpoint.

  • /api/posts/123: Might return the post's title, content, author info, and a list of 50 comment IDs.

This can lead to two problems:

  1. Over-fetching: You get back more data than you need. Your mobile UI might only need the post's title, but you're forced to download the entire content and author info, wasting bandwidth.
  2. Under-fetching: You don't get enough data. To display the comments for the post, you now need to make a second request to /api/comments?post_id=123.

GraphQL: Ask for What You Need, Get Exactly That

GraphQL is a query language for your API. Instead of having many "dumb" endpoints, you typically have a single "smart" endpoint that can respond to complex queries.

The client describes the exact data it needs, including nested relationships, in a single request.

GraphQL Query Example:

GraphQL


query GetPostDetails {
  post(id: "123") {
    title
    author {
      name
    }
    comments {
      body
    }
  }
}

The server will respond with a JSON object that perfectly mirrors the shape of your query. No more, no less. This solves both over-fetching and under-fetching in one go.

Introducing Apollo Client

Apollo Client is a comprehensive state management library for React that specializes in managing GraphQL data. It handles fetching, caching, and updating your UI when data changes.

Setup: First, you need to wrap your application with the ApolloProvider and give it an instance of the client.

JavaScript


import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://api.example.com/graphql', // URL of your GraphQL endpoint
  cache: new InMemoryCache(),
});

function App() {
  return (
    <ApolloProvider client={client}>
      <YourComponents />
    </ApolloProvider>
  );
}

Fetching Data with useQuery

The useQuery hook executes a GraphQL query and manages the loading and error states for you.

JavaScript


import { gql, useQuery } from '@apollo/client';

// Define your query using the gql template literal
const GET_ROCKETS = gql`
  query GetRockets {
    rockets {
      id
      name
      type
    }
  }
`;

function RocketList() {
  const { loading, error, data } = useQuery(GET_ROCKETS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return (
    <ul>
      {data.rockets.map(({ id, name, type }) => (
        <li key={id}>{name} ({type})</li>
      ))}
    </ul>
  );
}

Modifying Data with useMutation

To change data (create, update, delete), you use the useMutation hook. It returns a tuple with a mutate function that you can call to trigger the mutation.

JavaScript


const ADD_TODO = gql`
  mutation AddTodo($text: String!) {
    addTodo(text: $text) {
      id
      text
      completed
    }
  }
`;

function AddTodoForm() {
  let input;
  const [addTodo, { loading, error }] = useMutation(ADD_TODO, {
    // After the mutation, refetch the list of todos to update the UI
    refetchQueries: [{ query: GET_TODOS }],
  });

  const handleSubmit = e => {
    e.preventDefault();
    addTodo({ variables: { text: input.value } });
    input.value = '';
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={node => { input = node; }} />
      <button type="submit" disabled={loading}>Add Todo</button>
      {error && <p>Error adding todo</p>}
    </form>
  );
}