coderain guide

How to Incorporate GraphQL in a React Frontend

In the world of modern web development, efficient data fetching is critical to building responsive and user-friendly applications. For years, REST APIs have been the go-to solution, but they often suffer from limitations like over-fetching (retrieving more data than needed) or under-fetching (needing multiple requests to get required data). Enter **GraphQL**—a query language for APIs that lets clients request exactly the data they need, making data fetching more efficient and flexible. When paired with **React**, a library for building user interfaces, GraphQL becomes even more powerful. React’s component-based architecture and GraphQL’s ability to tailor data requests to component needs create a seamless developer experience. In this guide, we’ll walk through the step-by-step process of integrating GraphQL into a React frontend, from setting up your project to handling advanced features like caching and real-time updates.

Table of Contents

  1. Prerequisites
  2. Setting Up the React Project
  3. Setting Up a GraphQL API (Backend)
  4. Choosing a GraphQL Client
  5. Integrating Apollo Client into React
  6. Fetching Data with useQuery
  7. Mutating Data with useMutation
  8. Handling Loading and Error States
  9. Advanced Topics
  10. Conclusion
  11. 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:

ClientUse CaseProsCons
Apollo ClientMost React apps (full-featured)Easy setup, caching, hooks, dev toolsLarger bundle size (but tree-shakable)
RelayFacebook-scale apps (optimized)Advanced caching, fragment compositionSteeper learning curve
urqlLightweight apps (minimalist)Small bundle size, flexibleFewer 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 update function ensures the Apollo Client cache is updated after the mutation, so the BookList component re-renders with the new book.
  • variables passes 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:

  1. Set up a React project and GraphQL API.
  2. Integrate Apollo Client to connect React to the API.
  3. Fetch data with useQuery and mutate data with useMutation.
  4. 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).

References