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:
- 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.
- 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>
);
}