Using GraphQL in React (Apollo Client)

Loading

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 *