⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

GraphQL with Apollo Client: A Complete Guide

September 15, 2021
graphqlapolloreactapi
GraphQL with Apollo Client: A Complete Guide

GraphQL with Apollo Client: A Complete Guide

Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL.

Setup and Configuration

Installation

npm install @apollo/client graphql

Basic Setup

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

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

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

Queries

Basic Query

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

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

function UsersList() {
  const { loading, error, data } = useQuery(GET_USERS);

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

  return (
    <ul>
      {data.users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Query with Variables

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        title
      }
    }
  }
`;

function UserProfile({ userId }) {
  const { loading, data } = useQuery(GET_USER, {
    variables: { id: userId },
  });

  if (loading) return null;
  
  return (
    <div>
      <h1>{data.user.name}</h1>
      <p>{data.user.email}</p>
    </div>
  );
}

Conditional Queries

const { data } = useQuery(GET_USER, {
  variables: { id: userId },
  skip: !userId, // Skip query if no userId
  fetchPolicy: 'cache-and-network', // Refetch from network
});

Mutations

Basic Mutation

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

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(input: { name: $name, email: $email }) {
      id
      name
      email
    }
  }
`;

function CreateUserForm() {
  const [createUser, { loading, error }] = useMutation(CREATE_USER);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    
    try {
      await createUser({
        variables: {
          name: formData.get('name'),
          email: formData.get('email'),
        },
      });
    } catch (err) {
      console.error(err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" />
      <input name="email" placeholder="Email" />
      <button type="submit" disabled={loading}>
        Create User
      </button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

Updating Cache After Mutation

const [createUser] = useMutation(CREATE_USER, {
  update(cache, { data: { createUser } }) {
    cache.modify({
      fields: {
        users(existingUsers = []) {
          const newUserRef = cache.writeFragment({
            data: createUser,
            fragment: gql`
              fragment NewUser on User {
                id
                name
                email
              }
            `,
          });
          return [...existingUsers, newUserRef];
        },
      },
    });
  },
});

// Or refetch queries
const [createUser] = useMutation(CREATE_USER, {
  refetchQueries: [
    { query: GET_USERS },
  ],
});

Caching

Cache Policies

const { data } = useQuery(GET_USERS, {
  fetchPolicy: 'cache-first', // Default - use cache if available
  // 'cache-only' - never fetch from network
  // 'network-only' - always fetch, don't use cache
  // 'no-cache' - don't save to cache
  // 'cache-and-network' - show cache, then update with network
});

Cache Configuration

const client = new ApolloClient({
  uri: 'https://api.example.com/graphql',
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          users: {
            // Merge incoming data with existing
            merge(existing = [], incoming) {
              return [...existing, ...incoming];
            },
          },
        },
      },
      User: {
        keyFields: ['id'], // Use id as unique key
        fields: {
          name: {
            // Transform field
            read(name) {
              return name?.toUpperCase();
            },
          },
        },
      },
    },
  }),
});

Subscriptions

Setting Up WebSocket

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

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

const wsLink = new WebSocketLink({
  uri: 'ws://api.example.com/graphql',
  options: {
    reconnect: true,
  },
});

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(),
});

Using Subscriptions

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

const USER_UPDATED = gql`
  subscription OnUserUpdated {
    userUpdated {
      id
      name
      email
    }
  }
`;

function UserUpdates() {
  const { data, loading } = useSubscription(USER_UPDATED);

  if (loading) return null;

  return <p>User updated: {data.userUpdated.name}</p>;
}

Pagination

Cursor-Based Pagination

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

function PostsList() {
  const { data, loading, fetchMore } = useQuery(GET_POSTS);

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

  return (
    <div>
      {data?.posts.edges.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
      {data?.posts.pageInfo.hasNextPage && (
        <button onClick={loadMore} disabled={loading}>
          Load More
        </button>
      )}
    </div>
  );
}

Error Handling

const { loading, error, data } = useQuery(GET_USERS, {
  onError: (error) => {
    console.error('Query error:', error);
  },
  errorPolicy: 'all', // Show partial data even with errors
});

// Global error handling
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) =>
      console.log(`GraphQL error: ${message}`)
    );
  }
  if (networkError) {
    console.log(`Network error: ${networkError}`);
  }
});

Local State Management

// Define local-only field
const GET_LOCAL_DATA = gql`
  query GetLocalData {
    isLoggedIn @client
    currentUser @client {
      id
      name
    }
  }
`;

// Update local state
const [updateUser] = useMutation(gql`
  mutation UpdateCurrentUser($user: User!) {
    updateCurrentUser(user: $user) @client
  }
`);

// In cache
const client = new ApolloClient({
  cache: new InMemoryCache(),
  resolvers: {
    Query: {
      isLoggedIn: () => !!localStorage.getItem('token'),
      currentUser: () => {
        const user = localStorage.getItem('user');
        return user ? JSON.parse(user) : null;
      },
    },
    Mutation: {
      updateCurrentUser: (_, { user }, { cache }) => {
        cache.writeQuery({
          query: GET_LOCAL_DATA,
          data: { currentUser: user },
        });
        return user;
      },
    },
  },
});

Optimistic UI

const UPDATE_USER = gql`
  mutation UpdateUser($id: ID!, $name: String!) {
    updateUser(id: $id, name: $name) {
      id
      name
    }
  }
`;

const [updateUser] = useMutation(UPDATE_USER, {
  optimisticResponse: (variables) => ({
    updateUser: {
      id: variables.id,
      name: variables.name,
      __typename: 'User',
    },
  }),
  update(cache, { data: { updateUser } }) {
    cache.writeFragment({
      id: `User:${updateUser.id}`,
      fragment: gql`
        fragment UpdatedUser on User {
          name
        }
      `,
      data: { name: updateUser.name },
    });
  },
});

Best Practices

  1. Use fragments for reusable field selections
  2. Implement proper error handling
  3. Use optimistic UI for better UX
  4. Configure cache policies for your needs
  5. Handle pagination properly
  6. Use TypeScript with generated types
import { gql, useQuery } from '@apollo/client';
import { GetUsersQuery, GetUsersQueryVariables } from './generated';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
    }
  }
`;

function useUsers() {
  return useQuery<GetUsersQuery, GetUsersQueryVariables>(GET_USERS);
}

Conclusion

Apollo Client provides a powerful solution for managing GraphQL data in React applications. Its caching, optimistic UI, and real-time capabilities make it an excellent choice for modern web applications.

Share:

💬 Comments