Apollo Client for GraphQL Integration

Loading

Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Here’s how to effectively integrate Apollo Client in your React applications.

1. Setup and Configuration

Basic Apollo Client Setup

import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://your-graphql-endpoint.com/graphql',
  cache: new InMemoryCache(),
});

function App() {
  return (
    <ApolloProvider client={client}>
      <YourAppComponent />
    </ApolloProvider>
  );
}

Advanced Configuration

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

const httpLink = createHttpLink({
  uri: 'https://api.example.com/graphql',
});

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('authToken');
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    }
  };
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          posts: {
            merge(existing = [], incoming) {
              return [...existing, ...incoming];
            }
          }
        }
      }
    }
  }),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network',
    },
  },
});

2. Querying Data

Basic Query

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

const GET_POSTS = gql`
  query GetPosts {
    posts {
      id
      title
      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>By {post.author.name}</p>
        </li>
      ))}
    </ul>
  );
}

Paginated Query

const GET_PAGINATED_POSTS = gql`
  query GetPaginatedPosts($offset: Int!, $limit: Int!) {
    posts(offset: $offset, limit: $limit) {
      id
      title
    }
  }
`;

function PaginatedPostList() {
  const { data, loading, fetchMore } = useQuery(GET_PAGINATED_POSTS, {
    variables: { offset: 0, limit: 10 },
  });

  const loadMore = () => {
    fetchMore({
      variables: {
        offset: data.posts.length,
      },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) return prev;
        return {
          posts: [...prev.posts, ...fetchMoreResult.posts],
        };
      },
    });
  };

  return (
    <div>
      {/* Render posts */}
      <button onClick={loadMore} disabled={loading}>
        {loading ? 'Loading...' : 'Load More'}
      </button>
    </div>
  );
}

3. Mutations

Basic Mutation

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

const ADD_POST = gql`
  mutation AddPost($title: String!, $content: String!) {
    addPost(title: $title, content: $content) {
      id
      title
      content
    }
  }
`;

function AddPostForm() {
  const [addPost, { loading, error }] = useMutation(ADD_POST, {
    refetchQueries: ['GetPosts'], // Refetch after mutation
  });
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    addPost({ variables: { title, content } });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Title"
      />
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="Content"
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Adding...' : 'Add Post'}
      </button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

Optimistic UI Update

const ADD_COMMENT = gql`
  mutation AddComment($postId: ID!, $content: String!) {
    addComment(postId: $postId, content: $content) {
      id
      content
      createdAt
    }
  }
`;

function CommentForm({ postId }) {
  const [addComment] = useMutation(ADD_COMMENT, {
    optimisticResponse: {
      addComment: {
        id: 'temp-id',
        content: 'Optimistic comment',
        createdAt: new Date().toISOString(),
        __typename: 'Comment',
      },
    },
    update(cache, { data: { addComment } }) {
      cache.modify({
        fields: {
          comments(existingComments = []) {
            const newCommentRef = cache.writeFragment({
              data: addComment,
              fragment: gql`
                fragment NewComment on Comment {
                  id
                  content
                  createdAt
                }
              `,
            });
            return [...existingComments, newCommentRef];
          },
        },
      });
    },
  });

  // Form implementation...
}

4. Local State Management

Managing Local State

const GET_VISIBILITY = gql`
  query GetVisibility {
    visibilityFilter @client
  }
`;

const SET_VISIBILITY = gql`
  mutation SetVisibility($filter: String!) {
    setVisibilityFilter(filter: $filter) @client
  }
`;

function VisibilityFilter() {
  const { data } = useQuery(GET_VISIBILITY);
  const [setVisibility] = useMutation(SET_VISIBILITY);

  return (
    <div>
      <button onClick={() => setVisibility({ variables: { filter: 'SHOW_ALL' } })}>
        All
      </button>
      <button onClick={() => setVisibility({ variables: { filter: 'SHOW_ACTIVE' } })}>
        Active
      </button>
    </div>
  );
}

// In your Apollo Client setup
const client = new ApolloClient({
  // ...other config
  resolvers: {
    Mutation: {
      setVisibilityFilter: (_, { filter }, { cache }) => {
        cache.writeQuery({
          query: GET_VISIBILITY,
          data: { visibilityFilter: filter },
        });
        return null;
      },
    },
  },
});

5. Advanced Features

Error Handling

import { ApolloError } 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 (networkError) {
    console.error(`[Network error]: ${networkError}`);
  }
});

// Add to your Apollo Client configuration
const client = new ApolloClient({
  link: from([errorLink, authLink.concat(httpLink)]),
  // ...other config
});

Real-time Updates with Subscriptions

import { split, HttpLink, ApolloClient } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';

const wsLink = new WebSocketLink({
  uri: 'wss://your-graphql-endpoint.com/subscriptions',
  options: {
    reconnect: true,
    connectionParams: {
      authToken: localStorage.getItem('authToken'),
    },
  },
});

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink,
);

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

// Subscription component
const NEW_COMMENTS = gql`
  subscription OnNewComment($postId: ID!) {
    newComment(postId: $postId) {
      id
      content
      author {
        name
      }
    }
  }
`;

function CommentSubscription({ postId }) {
  const { data, loading } = useSubscription(NEW_COMMENTS, {
    variables: { postId },
    onSubscriptionData: ({ client, subscriptionData }) => {
      // Handle new comment
    },
  });

  // Render comments...
}

6. Performance Optimization

Query Batching

import { BatchHttpLink } from '@apollo/client/link/batch-http';

const batchLink = new BatchHttpLink({
  uri: 'https://api.example.com/graphql',
  batchMax: 5, // Max operations per batch
  batchInterval: 20, // Wait time in ms
});

const client = new ApolloClient({
  link: batchLink,
  cache: new InMemoryCache(),
});

Persisted Queries

import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';

const persistedQueriesLink = createPersistedQueryLink({
  sha256,
  useGETForHashedQueries: true,
});

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

Cache Normalization

const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      Product: {
        keyFields: ["id", "sku"], // Multiple key fields
      },
      User: {
        fields: {
          posts: {
            merge(existing = [], incoming) {
              return [...existing, ...incoming];
            },
          },
        },
      },
    },
  }),
});

7. Testing Components

Mocking Apollo Client in Tests

import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';

const mocks = [
  {
    request: {
      query: GET_POSTS,
    },
    result: {
      data: {
        posts: [
          { id: '1', title: 'Test Post', author: { name: 'Test User' } },
        ],
      },
    },
  },
];

test('renders posts', async () => {
  render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <PostList />
    </MockedProvider>
  );

  expect(await screen.findByText('Test Post')).toBeInTheDocument();
});

Testing Mutations

const mutationMock = [
  {
    request: {
      query: ADD_POST,
      variables: { title: 'Test', content: 'Content' },
    },
    result: {
      data: {
        addPost: { id: '1', title: 'Test', content: 'Content' },
      },
    },
  },
];

test('submits mutation', async () => {
  render(
    <MockedProvider mocks={mutationMock}>
      <AddPostForm />
    </MockedProvider>
  );

  fireEvent.change(screen.getByPlaceholderText('Title'), {
    target: { value: 'Test' },
  });
  fireEvent.click(screen.getByText('Add Post'));

  await waitFor(() => {
    expect(mutationMock[0].result).toHaveBeenCalled();
  });
});

Best Practices

  1. Query Colocation: Keep queries close to components that use them
  2. Fragment Usage: Reuse common field selections with fragments
  3. Error Boundaries: Wrap components with error boundaries
  4. Type Generation: Use GraphQL Code Generator for TypeScript types
  5. Pagination: Implement cursor-based pagination for large datasets
  6. Cache Strategy: Choose appropriate fetch policies per use case
  7. Bundle Optimization: Load only necessary Apollo Client features

By following these patterns and leveraging Apollo Client’s powerful features, you can build robust, performant React applications with GraphQL that scale effectively.

Leave a Reply

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