Table of Contents
- Prerequisites
- Setting Up the React Project
- Understanding GraphQL Basics (Recap)
- Choosing a GraphQL Client
- Setting Up Apollo Client
- Creating a GraphQL API (Backend)
- Fetching Data with React and Apollo Client
- Mutating Data with React and Apollo Client
- Handling Loading and Error States
- Caching in Apollo Client
- Advanced Topics
- Conclusion
- References
Prerequisites
Before diving in, ensure you have the following:
- Basic knowledge of React (components, hooks, state management).
- Familiarity with JavaScript (ES6+ features like arrow functions, destructuring).
- Node.js (v14+ recommended) and npm/yarn installed.
- A basic understanding of GraphQL (queries, mutations, schemas). If you need a refresher, check out the GraphQL official docs.
Setting Up the React Project
First, let’s create a new React project using create-react-app, a tool that sets up a modern React environment with zero configuration.
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 use Apollo Client (the most popular GraphQL client for React) and graphql (the core GraphQL library). Install them with:
npm install @apollo/client graphql
Understanding GraphQL Basics (Recap)
GraphQL is a query language for APIs, designed to let clients request exactly the data they need. Key concepts:
- Query: A read operation to fetch data (like GET in REST).
- Mutation: A write operation to create/update/delete data (like POST/PUT/DELETE in REST).
- Schema: A contract between client and server defining available data types and operations.
- Resolver: Functions on the server that return data for fields in the schema.
Example query to fetch a list of books:
query GetBooks {
books {
id
title
author
}
}
Example mutation to add a book:
mutation AddBook($title: String!, $author: String!) {
addBook(title: $title, author: $author) {
id
title
author
}
}
Choosing a GraphQL Client
When integrating GraphQL with React, you’ll need a client to handle:
- Sending queries/mutations to the server.
- Caching fetched data.
- Managing loading/error states.
- Updating the UI when data changes.
Popular Options:
- Apollo Client: The most widely used. It’s flexible, well-documented, and works with any GraphQL server. Ideal for beginners and production apps.
- Relay: Developed by Facebook. More opinionated, optimized for performance, but has a steeper learning curve. Best for large-scale apps.
We’ll use Apollo Client in this guide due to its simplicity and robust feature set.
Setting Up Apollo Client
Apollo Client requires configuration to connect to your GraphQL server. Here’s how to set it up:
Step 1: Initialize Apollo Client
Create a new file src/apollo-client.js and add:
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
// Initialize Apollo Client
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql', // URL of your GraphQL server
cache: new InMemoryCache(), // Stores fetched data locally
});
export default client;
Step 2: Wrap React App with ApolloProvider
In src/index.js, wrap your App component with ApolloProvider to make the client available to all components:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import client from './apollo-client';
import { ApolloProvider } from '@apollo/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
Now, every component in your app can access Apollo Client’s features!
Creating a GraphQL API (Backend)
To fetch/mutate data, we need a GraphQL server. Let’s build a simple one using Apollo Server (a GraphQL server for Node.js) and Express.
Step 1: Set Up the Server
Create a new folder for the server and initialize it:
mkdir graphql-server
cd graphql-server
npm init -y
npm install apollo-server-express express cors
Step 2: Define the Schema and Resolvers
Create server.js in the graphql-server folder:
const express = require('express');
const { ApolloServer, gql } = require('apollo-server-express');
const cors = require('cors');
// Sample data (in-memory "database")
let books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' },
{ id: '2', title: '1984', author: 'George Orwell' },
];
// Schema: Defines data types and operations
const typeDefs = gql`
type Book {
id: ID!
title: String!
author: String!
}
type Query {
books: [Book!]! # Fetch all books
book(id: ID!): Book # Fetch a single book by ID
}
type Mutation {
addBook(title: String!, author: String!): Book! # Add a new book
}
`;
// Resolvers: Functions that return data for schema fields
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;
},
},
};
// Set up Express server
async function startServer() {
const app = express();
app.use(cors()); // Allow cross-origin requests (from React app)
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
server.applyMiddleware({ app });
const PORT = 4000;
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}${server.graphqlPath}`);
});
}
startServer();
Step 3: Run the Server
Start the server with:
node server.js
You should see: Server running at http://localhost:4000/graphql. Visit this URL in your browser to access Apollo Server’s built-in IDE (GraphQL Playground), where you can test queries/mutations.
Fetching Data with React and Apollo Client
Now that our server is running, let’s fetch data in React using Apollo Client’s useQuery hook.
Step 1: Write a GraphQL Query
Create a new component src/components/BookList.js to display a list of books. First, define the query:
import { gql, useQuery } from '@apollo/client';
// Define the query
const GET_BOOKS = gql`
query GetBooks {
books {
id
title
author
}
}
`;
Step 2: Use the useQuery Hook
The useQuery hook executes the query and returns:
data: The fetched data (if successful).loading: A boolean indicating if the request is in progress.error: An error object (if the request fails).
Update BookList.js:
import { gql, useQuery } from '@apollo/client';
const GET_BOOKS = gql`
query GetBooks {
books {
id
title
author
}
}
`;
function BookList() {
const { loading, error, data } = useQuery(GET_BOOKS);
if (loading) return <p>Loading books...</p>;
if (error) return <p>Error: {error.message}</p>;
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: Render the Component
Update src/App.js to include BookList:
import BookList from './components/BookList';
function App() {
return (
<div className="App" style={{ padding: '2rem' }}>
<h1>GraphQL + React Demo</h1>
<BookList />
</div>
);
}
export default App;
Run your React app with npm start. You should see a list of books fetched from the server!
Mutating Data with React and Apollo Client
To add, update, or delete data, use Apollo Client’s useMutation hook. Let’s build a form to add new books.
Step 1: Write a Mutation
Create src/components/AddBookForm.js and define the mutation:
import { gql, useMutation } from '@apollo/client';
import { useState } from 'react';
// Define the mutation
const ADD_BOOK = gql`
mutation AddBook($title: String!, $author: String!) {
addBook(title: $title, author: $author) {
id
title
author
}
}
`;
Step 2: Use the useMutation Hook
The useMutation hook returns a function to execute the mutation and an object with mutation status.
Update AddBookForm.js:
import { gql, useMutation } from '@apollo/client';
import { useState } from 'react';
import { GET_BOOKS } from './BookList'; // Import the query to update cache
const ADD_BOOK = gql`
mutation AddBook($title: String!, $author: String!) {
addBook(title: $title, author: $author) {
id
title
author
}
}
`;
function AddBookForm() {
const [title, setTitle] = useState('');
const [author, setAuthor] = useState('');
// Define the mutation function
const [addBook, { loading, error }] = useMutation(ADD_BOOK, {
// Update the cache to reflect the new book (so BookList re-renders)
update(cache, { data: { addBook } }) {
const { books } = cache.readQuery({ query: GET_BOOKS });
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} style={{ margin: '2rem 0' }}>
<h3>Add a New Book</h3>
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
style={{ marginRight: '1rem', padding: '0.5rem' }}
/>
<input
type="text"
placeholder="Author"
value={author}
onChange={(e) => setAuthor(e.target.value)}
required
style={{ marginRight: '1rem', padding: '0.5rem' }}
/>
<button type="submit" disabled={loading}>
{loading ? 'Adding...' : 'Add Book'}
</button>
{error && <p style={{ color: 'red' }}>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. variablespass dynamic values (title/author) to the mutation.
Step 3: Add the Form to App.js
Update src/App.js to include AddBookForm:
import BookList from './components/BookList';
import AddBookForm from './components/AddBookForm';
function App() {
return (
<div className="App" style={{ padding: '2rem' }}>
<h1>GraphQL + React Demo</h1>
<AddBookForm />
<BookList />
</div>
);
}
export default App;
Now, you can add books via the form, and the list will update automatically!
Handling Loading and Error States
Apollo Client’s useQuery and useMutation hooks provide loading and error states to keep users informed. Let’s enhance the BookList component with better loading/error handling:
function BookList() {
const { loading, error, data } = useQuery(GET_BOOKS);
if (loading) {
return <div style={{ textAlign: 'center', padding: '2rem' }}>Loading books... 📚</div>;
}
if (error) {
return (
<div style={{ color: 'red', padding: '2rem' }}>
Error: {error.message}. Please check the server.
</div>
);
}
if (data.books.length === 0) { // Handle empty list
return <p>No books found. Add a book above!</p>;
}
return (
<div>
<h2>Book List</h2>
<ul style={{ listStyle: 'none', padding: 0 }}>
{data.books.map((book) => (
<li key={book.id} style={{ padding: '0.5rem', borderBottom: '1px solid #eee' }}>
<strong>{book.title}</strong> by {book.author}
</li>
))}
</ul>
</div>
);
}
Caching in Apollo Client
Apollo Client automatically caches fetched data, so subsequent requests for the same data return immediately from the cache (no network call). This improves performance and reduces server load.
How It Works:
- When you fetch data with
useQuery, Apollo Client stores it in a normalized cache (keyed by type and ID). - If you fetch the same data again (e.g., in another component), Apollo returns the cached data instantly.
- Mutations can update the cache manually (as we did with the
updatefunction inAddBookForm), ensuring the UI stays in sync.
Example: If you fetch a book by ID in one component, and then fetch it again elsewhere, Apollo uses the cache.
Advanced Topics
Subscriptions (Real-Time Data)
For real-time features (e.g., chat apps), use GraphQL Subscriptions with WebSockets. Apollo Client supports subscriptions via graphql-ws. Check the Apollo Subscriptions docs for setup.
Fragments
Reuse query logic across components with fragments:
fragment BookDetails on Book {
id
title
author
}
query GetBooks {
books { ...BookDetails }
}
Variables
Pass dynamic values to queries/mutations using variables (as we did in the addBook mutation).
Conclusion
Integrating GraphQL with React using Apollo Client simplifies data fetching, reduces over-fetching, and improves state management. In this guide, we:
- Set up a React project and Apollo Client.
- Built a simple GraphQL server with Apollo Server.
- Fetched data with
useQueryand mutated data withuseMutation. - Handled loading/error states and leveraged Apollo’s caching.
GraphQL’s flexibility combined with React’s component model makes building data-driven apps faster and more intuitive. For next steps, explore advanced features like subscriptions, error handling middleware, or using Apollo Client with TypeScript.