Table of Contents
- Prerequisites
- Setting Up the React Project
- Setting Up a GraphQL API (Backend)
- Choosing a GraphQL Client
- Integrating Apollo Client into React
- Fetching Data with
useQuery - Mutating Data with
useMutation - Handling Loading and Error States
- Advanced Topics
- Conclusion
- References
Prerequisites
Before diving in, ensure you have the following tools and knowledge:
- Basic React Knowledge: Familiarity with React components, hooks (e.g.,
useState,useEffect), and JSX. - Node.js & npm/yarn: Installed on your machine (v14+ recommended).
- Code Editor: VS Code or your preferred editor.
- GraphQL Basics: Understanding of core GraphQL concepts (queries, mutations, schemas, resolvers) is helpful but not required—we’ll cover key concepts as we go.
Setting Up the React Project
First, let’s create a new React project. We’ll use create-react-app for simplicity, but you could also use Vite or another bundler.
Step 1: Create the React App
Open your terminal and run:
npx create-react-app graphql-react-demo
cd graphql-react-demo
Step 2: Install Dependencies
We’ll need two key packages to work with GraphQL in React:
@apollo/client: A full-featured GraphQL client for React (we’ll use this as our client).graphql: The core GraphQL library for parsing queries.
Install them with:
npm install @apollo/client graphql
Setting Up a GraphQL API (Backend)
To fetch data, we need a GraphQL API. For this guide, we’ll set up a simple local API using Apollo Server (a popular GraphQL server implementation) with Express.
Step 1: Create a Backend Directory
In a separate terminal, create a new directory for the backend and initialize it:
mkdir graphql-server
cd graphql-server
npm init -y
Step 2: Install Backend Dependencies
Install Apollo Server, Express, and related packages:
npm install apollo-server-express express cors
Step 3: Define the GraphQL Schema and Resolvers
Create a server.js file in the graphql-server directory:
const express = require('express');
const { ApolloServer, gql } = require('apollo-server-express');
const cors = require('cors');
// Sample data (replace with a database in production)
let books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' },
{ id: '2', title: '1984', author: 'George Orwell' },
];
// GraphQL Schema: Defines the data types and operations (queries/mutations)
const typeDefs = gql`
type Book {
id: ID!
title: String!
author: String!
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
type Mutation {
addBook(title: String!, author: String!): Book!
}
`;
// Resolvers: Define how to fetch/modify data for each schema field
const resolvers = {
Query: {
books: () => books,
book: (parent, { id }) => books.find(book => book.id === id),
},
Mutation: {
addBook: (parent, { title, author }) => {
const newBook = { id: String(books.length + 1), title, author };
books.push(newBook);
return newBook;
},
},
};
// Initialize Express app
const app = express();
app.use(cors()); // Enable CORS for cross-origin requests
// Set up Apollo Server
const server = new ApolloServer({ typeDefs, resolvers });
// Start the server
async function startServer() {
await server.start();
server.applyMiddleware({ app });
const PORT = 4000;
app.listen(PORT, () => {
console.log(`GraphQL API running at http://localhost:${PORT}${server.graphqlPath}`);
});
}
startServer();
Step 4: Run the Backend
Start the server with:
node server.js
You should see:
GraphQL API running at http://localhost:4000/graphql
Visit http://localhost:4000/graphql in your browser to access the Apollo Studio sandbox, where you can test queries and mutations (e.g., run query { books { title author } } to fetch books).
Choosing a GraphQL Client
To connect React to your GraphQL API, you need a client. The most popular options are:
| Client | Use Case | Pros | Cons |
|---|---|---|---|
| Apollo Client | Most React apps (full-featured) | Easy setup, caching, hooks, dev tools | Larger bundle size (but tree-shakable) |
| Relay | Facebook-scale apps (optimized) | Advanced caching, fragment composition | Steeper learning curve |
| urql | Lightweight apps (minimalist) | Small bundle size, flexible | Fewer built-in features (e.g., caching) |
For this guide, we’ll use Apollo Client due to its simplicity and robust feature set.
Integrating Apollo Client into React
Apollo Client acts as a middleware between your React app and GraphQL API. Here’s how to set it up:
Step 1: Initialize Apollo Client
Create a src/apollo-client.js file to configure the client:
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
// HTTP connection to the API
const httpLink = createHttpLink({
uri: 'http://localhost:4000/graphql', // Your GraphQL API URI
});
// Add authentication headers (optional, skip if not needed)
const authLink = setContext((_, { headers }) => {
// Get the authentication token from local storage if it exists
const token = localStorage.getItem('authToken');
// Return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
// Combine the authentication link with the HTTP link
const link = authLink.concat(httpLink);
// Initialize Apollo Client with the API URI and cache
const client = new ApolloClient({
link,
cache: new InMemoryCache(), // In-memory cache for storing fetched data
});
export default client;
Step 2: Wrap the App with ApolloProvider
In src/index.js, wrap your React app with ApolloProvider to make the client available to all components:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import client from './apollo-client';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
{/* Wrap App with ApolloProvider to inject the client */}
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
);
Now, all components in your app can access the Apollo Client to fetch or mutate data.
Fetching Data with useQuery
The useQuery hook from Apollo Client lets you fetch data from your GraphQL API and handle loading/error states. Let’s create a component to display the list of books.
Step 1: Define a Query
Create a src/queries.js file to store your GraphQL queries:
import { gql } from '@apollo/client';
// Query to fetch all books
export const GET_BOOKS = gql`
query GetBooks {
books {
id
title
author
}
}
`;
Step 2: Create a BookList Component
Create src/components/BookList.js:
import React from 'react';
import { useQuery } from '@apollo/client';
import { GET_BOOKS } from '../queries';
const BookList = () => {
// Fetch data with useQuery
const { loading, error, data } = useQuery(GET_BOOKS);
if (loading) return <p>Loading books...</p>; // Show loading state
if (error) return <p>Error: {error.message}</p>; // Show error state
return (
<div>
<h2>Book List</h2>
<ul>
{data.books.map((book) => (
<li key={book.id}>
<strong>{book.title}</strong> by {book.author}
</li>
))}
</ul>
</div>
);
};
export default BookList;
Step 3: Update App.js
Import and render BookList in src/App.js:
import './App.css';
import BookList from './components/BookList';
function App() {
return (
<div className="App">
<h1>My GraphQL React App</h1>
<BookList />
</div>
);
}
export default App;
Run your React app with npm start. You should see the list of books fetched from your API!
Mutating Data with useMutation
To modify data (e.g., add, update, delete), use the useMutation hook. Let’s add a form to create new books.
Step 1: Define a Mutation
Add this to src/queries.js:
// Mutation to add a new book
export const ADD_BOOK = gql`
mutation AddBook($title: String!, $author: String!) {
addBook(title: $title, author: $author) {
id
title
author
}
}
`;
Step 2: Create a AddBookForm Component
Create src/components/AddBookForm.js:
import React, { useState } from 'react';
import { useMutation } from '@apollo/client';
import { ADD_BOOK, GET_BOOKS } from '../queries';
const AddBookForm = () => {
const [title, setTitle] = useState('');
const [author, setAuthor] = useState('');
// Define the mutation hook
const [addBook, { loading, error }] = useMutation(ADD_BOOK, {
// Update the cache after mutation to reflect new data
update(cache, { data: { addBook } }) {
// Read the existing books from the cache
const { books } = cache.readQuery({ query: GET_BOOKS });
// Write the updated books array back to the cache
cache.writeQuery({
query: GET_BOOKS,
data: { books: [...books, addBook] },
});
},
});
const handleSubmit = (e) => {
e.preventDefault();
addBook({ variables: { title, author } }); // Execute mutation
setTitle(''); // Reset form
setAuthor('');
};
return (
<form onSubmit={handleSubmit}>
<h3>Add New Book</h3>
<div>
<label>Title:</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div>
<label>Author:</label>
<input
type="text"
value={author}
onChange={(e) => setAuthor(e.target.value)}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Adding...' : 'Add Book'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
};
export default AddBookForm;
Key Notes:
- The
updatefunction ensures the Apollo Client cache is updated after the mutation, so theBookListcomponent re-renders with the new book. variablespasses form data to the mutation.
Step 3: Update App.js to Include the Form
Import AddBookForm and render it in App.js:
import './App.css';
import BookList from './components/BookList';
import AddBookForm from './components/AddBookForm';
function App() {
return (
<div className="App">
<h1>My GraphQL React App</h1>
<AddBookForm />
<BookList />
</div>
);
}
export default App;
Now, submit the form with a title and author—the book will appear in the list immediately!
Handling Loading and Error States
A smooth user experience requires handling loading and error states gracefully. Here are best practices:
Loading States
- Use a spinner or skeleton loader while data is fetched.
- Disable buttons during mutations to prevent duplicate submissions.
Example with a spinner (using react-spinners):
npm install react-spinners
Update BookList.js:
import { ClipLoader } from 'react-spinners';
// ...
if (loading) return <ClipLoader color="#36d7b7" size={50} />;
// ...
Error States
- Display user-friendly error messages (avoid exposing internal details).
- Add retry buttons for transient errors.
Example error handling in BookList.js:
if (error) return (
<div className="error">
<p>Oops! Could not load books.</p>
<button onClick={() => refetch()}>Try Again</button>
</div>
);
(Note: refetch() is a function returned by useQuery to retry the request.)
Advanced Topics
Caching with Apollo Client
Apollo Client automatically caches fetched data, reducing redundant network requests. By default, it uses a normalized cache (storing data by unique ID) for efficient updates.
- View the cache: Use the Apollo Client DevTools (Chrome/Firefox extension) to inspect cached data.
- Force a refetch: Use
useQuery({ fetchPolicy: 'network-only' })to bypass the cache. - Invalidate the cache: Use
client.invalidateQueries({ query: GET_BOOKS })to mark cached data as stale and refetch.
Reusing Queries with Fragments
Fragments let you reuse fields across queries/mutations, reducing duplication.
Example fragment in src/queries.js:
export const BOOK_FIELDS = gql`
fragment BookFields on Book {
id
title
author
}
`;
// Reuse the fragment in a query
export const GET_BOOKS = gql`
query GetBooks {
books {
...BookFields
}
}
${BOOK_FIELDS}
`;
// Reuse in a mutation
export const ADD_BOOK = gql`
mutation AddBook($title: String!, $author: String!) {
addBook(title: $title, author: $author) {
...BookFields
}
}
${BOOK_FIELDS}
`;
Real-Time Data with Subscriptions
For real-time features (e.g., chat apps), use GraphQL subscriptions (powered by WebSockets).
Step 1: Update the Backend
Add subscriptions to server.js (requires graphql-ws):
npm install graphql-ws ws
Update the server to support subscriptions (see Apollo Server Subscriptions docs for details).
Step 2: Use Subscriptions in React
Update Apollo Client to support subscriptions and use the useSubscription hook:
// In apollo-client.js, add a subscription link
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
const wsLink = new GraphQLWsLink(
createClient({ url: 'ws://localhost:4000/graphql' })
);
// Combine with HTTP link (use ws for subscriptions, http for queries/mutations)
const link = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
Then use useSubscription in a component to listen for new books:
import { useSubscription } from '@apollo/client';
import { BOOK_ADDED } from '../queries';
const BookSubscription = () => {
useSubscription(BOOK_ADDED, {
onSubscriptionData: ({ subscriptionData }) => {
const newBook = subscriptionData.data.bookAdded;
console.log('New book:', newBook);
// Update UI or cache
},
});
return null; // No UI needed—just listens for events
};
Conclusion
Integrating GraphQL into a React frontend streamlines data fetching, reduces over-fetching, and improves developer productivity. By following this guide, you’ve learned to:
- Set up a React project and GraphQL API.
- Integrate Apollo Client to connect React to the API.
- Fetch data with
useQueryand mutate data withuseMutation. - Handle loading/error states and leverage advanced features like caching and fragments.
GraphQL and React are a powerful combination—explore further with Apollo Client’s docs, experiment with subscriptions, or build a full-stack app with a database (e.g., PostgreSQL with Prisma).