Using GraphQL in React (Apollo Client)

GraphQL provides an efficient alternative to REST for data fetching in React applications. Apollo Client is the most popular GraphQL client for React. Here’s a comprehensive guide to integrating them:

1. Setup and Configuration

Installation

npm install @apollo/client graphql
# or
yarn add @apollo/client graphql

Apollo Client Setup

// src/apollo-client.js
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  createHttpLink
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';

const httpLink = createHttpLink({
  uri: 'https://your-graphql-endpoint.com/graphql',
});

const authLink = setContext((_, { headers }) => {
  // Get authentication token from storage if it exists
  const token = localStorage.getItem('authToken');

  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    }
  };
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
});

export default client;

Wrap Your App with ApolloProvider

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import client from './apollo-client';
import { ApolloProvider } from '@apollo/client';

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
);

2. Basic Queries

useQuery Hook

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

const GET_POSTS = gql`
  query GetPosts {
    posts {
      id
      title
      body
      author {
        name
      }
    }
  }
`;

function PostList() {
  const { loading, error, data } = useQuery(GET_POSTS);

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

  return (
    <ul>
      {data.posts.map((post) => (
        <li key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
          <p>By: {post.author.name}</p>
        </li>
      ))}
    </ul>
  );
}

Query with Variables

const GET_POST = gql`
  query GetPost($id: ID!) {
    post(id: $id) {
      id
      title
      body
    }
  }
`;

function PostDetail({ postId }) {
  const { loading, error, data } = useQuery(GET_POST, {
    variables: { id: postId },
  });

  // ...render logic
}

3. Mutations

useMutation Hook

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

const CREATE_POST = gql`
  mutation CreatePost($title: String!, $body: String!) {
    createPost(title: $title, body: $body) {
      id
      title
      body
    }
  }
`;

function CreatePostForm() {
  const [createPost, { loading, error }] = useMutation(CREATE_POST);
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const { data } = await createPost({
        variables: { title, body },
      });
      console.log('Post created:', data.createPost);
      // Reset form or redirect
    } catch (err) {
      console.error('Error creating post:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Title"
      />
      <textarea
        value={body}
        onChange={(e) => setBody(e.target.value)}
        placeholder="Body"
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create Post'}
      </button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

Optimistic UI Updates

const [createPost] = useMutation(CREATE_POST, {
  optimisticResponse: {
    __typename: 'Mutation',
    createPost: {
      __typename: 'Post',
      id: 'temp-id',
      title,
      body,
    },
  },
  update: (cache, { data: { createPost } }) => {
    cache.modify({
      fields: {
        posts(existingPosts = []) {
          const newPostRef = cache.writeFragment({
            data: createPost,
            fragment: gql`
              fragment NewPost on Post {
                id
                title
                body
              }
            `
          });
          return [...existingPosts, newPostRef];
        },
      },
    });
  },
});

4. Advanced Features

Pagination

const GET_POSTS = gql`
  query GetPosts($first: Int, $after: String) {
    posts(first: $first, after: $after) {
      edges {
        node {
          id
          title
        }
        cursor
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

function PostList() {
  const { loading, error, data, fetchMore } = useQuery(GET_POSTS, {
    variables: { first: 5 },
  });

  const loadMore = () => {
    fetchMore({
      variables: {
        after: data.posts.pageInfo.endCursor,
      },
    });
  };

  // ...render logic
  return (
    <div>
      {/* Post list */}
      {data?.posts?.pageInfo.hasNextPage && (
        <button onClick={loadMore} disabled={loading}>
          Load More
        </button>
      )}
    </div>
  );
}

Local State Management

// Add to your Apollo Client setup
const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache(),
  typeDefs: gql`
    extend type Query {
      isLoggedIn: Boolean!
    }
  `,
  resolvers: {
    Query: {
      isLoggedIn: () => !!localStorage.getItem('authToken'),
    },
  },
});

// Set initial state
client.writeQuery({
  query: gql`
    query GetAuthStatus {
      isLoggedIn
    }
  `,
  data: {
    isLoggedIn: !!localStorage.getItem('authToken'),
  },
});

// Usage in components
const { data } = useQuery(gql`
  query GetAuthStatus {
    isLoggedIn @client
  }
`);

5. Authentication

Login Mutation

const LOGIN = gql`
  mutation Login($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      token
      user {
        id
        name
      }
    }
  }
`;

function LoginForm() {
  const [login, { loading, error }] = useMutation(LOGIN, {
    onCompleted: ({ login }) => {
      localStorage.setItem('authToken', login.token);
      client.writeQuery({
        query: gql`
          query GetAuthStatus {
            isLoggedIn
          }
        `,
        data: { isLoggedIn: true },
      });
    },
  });

  // ...form handling
}

Protected Routes

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

const IS_LOGGED_IN = gql`
  query IsLoggedIn {
    isLoggedIn @client
  }
`;

function ProtectedRoute({ children }) {
  const { data } = useQuery(IS_LOGGED_IN);

  if (!data?.isLoggedIn) {
    return <Navigate to="/login" replace />;
  }

  return children;
}

6. Error Handling

Global Error Handling

import { ApolloClient, from, HttpLink } from '@apollo/client';
import { onError } from '@apollo/client/link/error';

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      console.error(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
      );
      if (message.includes('Unauthorized')) {
        // Handle token expiration
      }
    });
  }

  if (networkError) {
    console.error(`[Network error]: ${networkError}`);
  }
});

const client = new ApolloClient({
  link: from([errorLink, authLink.concat(httpLink)]),
  cache: new InMemoryCache(),
});

UI Error Boundaries

import { useQuery } from '@apollo/client';
import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function QueryWithErrorBoundary() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => window.location.reload()}
    >
      <PostList />
    </ErrorBoundary>
  );
}

Best Practices

  1. Code Organization:
  • Keep queries/mutations in separate files
  • Use fragments for shared fields
  • Organize by feature rather than by type
  1. Performance:
  • Use fetchPolicy wisely (cache-first is default)
  • Implement pagination for large datasets
  • Use @defer for non-critical data
  1. Testing:
  • Mock Apollo Client in tests
  • Use MockedProvider for component tests
  1. Security:
  • Never store sensitive data in client state
  • Implement proper authentication
  • Use persisted queries in production
  1. Developer Experience:
  • Use GraphQL Code Generator for TypeScript types
  • Set up GraphQL linting
  • Document your schema thoroughly

This comprehensive integration of Apollo Client with React provides a solid foundation for building performant, type-safe applications with GraphQL. The combination of declarative data fetching, real-time updates, and local state management makes Apollo Client a powerful tool for modern React development.

Leave a Reply

Your email address will not be published. Required fields are marked *