GraphQL

Building a GraphQL Client App

16 min Lesson 34 of 35

Building a GraphQL Client App

In this lesson, we'll build a complete React client application that consumes our GraphQL API. We'll use Apollo Client for state management, caching, and real-time updates.

Project Setup with Apollo Client

# Create React app npx create-react-app blog-client cd blog-client # Install Apollo Client dependencies npm install @apollo/client graphql npm install react-router-dom npm install @apollo/client-react-hooks
// src/index.js import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import { ApolloClient, InMemoryCache, ApolloProvider, createHttpLink, split, } from '@apollo/client'; import { setContext } from '@apollo/client/link/context'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { getMainDefinition } from '@apollo/client/utilities'; import { createClient } from 'graphql-ws'; import App from './App'; // HTTP link for queries and mutations const httpLink = createHttpLink({ uri: 'http://localhost:4000/graphql', }); // WebSocket link for subscriptions const wsLink = new GraphQLWsLink( createClient({ url: 'ws://localhost:4000/graphql', }) ); // Auth link to add JWT token const authLink = setContext((_, { headers }) => { const token = localStorage.getItem('token'); return { headers: { ...headers, authorization: token ? `Bearer ${token}` : '', }, }; }); // Split links based on operation type const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ); }, wsLink, authLink.concat(httpLink) ); // Create Apollo Client const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache({ typePolicies: { Query: { fields: { posts: { merge(existing = [], incoming) { return [...existing, ...incoming]; }, }, }, }, }, }), }); const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <ApolloProvider client={client}> <BrowserRouter> <App /> </BrowserRouter> </ApolloProvider> );
Apollo Client Setup: The client is configured with HTTP link for queries/mutations, WebSocket link for subscriptions, authentication middleware, and intelligent caching with type policies.

GraphQL Queries and Mutations

// src/graphql/queries.js import { gql } from '@apollo/client'; export const GET_POSTS = gql` query GetPosts($limit: Int, $offset: Int, $status: PostStatus) { posts(limit: $limit, offset: $offset, status: $status) { id title slug excerpt featuredImage status viewCount createdAt author { id username avatar } } } `; export const GET_POST = gql` query GetPost($slug: String!) { post(slug: $slug) { id title slug content excerpt featuredImage status viewCount tags createdAt updatedAt publishedAt author { id username fullName avatar bio } comments { id content isEdited createdAt author { id username avatar } replies { id content createdAt author { id username avatar } } } } } `; export const GET_ME = gql` query GetMe { me { id username email fullName bio avatar role } } `;
// src/graphql/mutations.js import { gql } from '@apollo/client'; export const LOGIN = gql` mutation Login($input: LoginInput!) { login(input: $input) { token user { id username email role } } } `; export const REGISTER = gql` mutation Register($input: RegisterInput!) { register(input: $input) { token user { id username email role } } } `; export const CREATE_POST = gql` mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id title slug status createdAt } } `; export const UPDATE_POST = gql` mutation UpdatePost($id: ID!, $input: UpdatePostInput!) { updatePost(id: $id, input: $input) { id title content status updatedAt } } `; export const DELETE_POST = gql` mutation DeletePost($id: ID!) { deletePost(id: $id) } `; export const CREATE_COMMENT = gql` mutation CreateComment($input: CreateCommentInput!) { createComment(input: $input) { id content createdAt author { id username avatar } } } `;

Querying Data in Components

// src/components/PostList.jsx import React from 'react'; import { useQuery } from '@apollo/client'; import { GET_POSTS } from '../graphql/queries'; import { Link } from 'react-router-dom'; function PostList() { const { loading, error, data, fetchMore } = useQuery(GET_POSTS, { variables: { limit: 10, offset: 0, status: 'PUBLISHED' }, }); if (loading) return <div>Loading posts...</div>; if (error) return <div>Error: {error.message}</div>; const loadMore = () => { fetchMore({ variables: { offset: data.posts.length, }, }); }; return ( <div className="post-list"> <h1>Blog Posts</h1> {data.posts.map(post => ( <article key={post.id} className="post-card"> {post.featuredImage && ( <img src={post.featuredImage} alt={post.title} /> )} <h2> <Link to={`/posts/${post.slug}`}>{post.title}</Link> </h2> <p>{post.excerpt}</p> <div className="post-meta"> <span>By {post.author.username}</span> <span>{new Date(post.createdAt).toLocaleDateString()}</span> <span>{post.viewCount} views</span> </div> </article> ))} <button onClick={loadMore}>Load More</button> </div> ); } export default PostList;

Creating and Updating Data

// src/components/CreatePost.jsx import React, { useState } from 'react'; import { useMutation } from '@apollo/client'; import { CREATE_POST } from '../graphql/mutations'; import { GET_POSTS } from '../graphql/queries'; import { useNavigate } from 'react-router-dom'; function CreatePost() { const navigate = useNavigate(); const [formData, setFormData] = useState({ title: '', content: '', excerpt: '', tags: '', }); const [createPost, { loading, error }] = useMutation(CREATE_POST, { onCompleted: (data) => { navigate(`/posts/${data.createPost.slug}`); }, refetchQueries: [{ query: GET_POSTS }], }); const handleSubmit = async (e) => { e.preventDefault(); const tags = formData.tags.split(',').map(tag => tag.trim()); await createPost({ variables: { input: { title: formData.title, content: formData.content, excerpt: formData.excerpt, tags, status: 'DRAFT', }, }, }); }; return ( <form onSubmit={handleSubmit}> <h2>Create New Post</h2> <input type="text" placeholder="Title" value={formData.title} onChange={(e) => setFormData({ ...formData, title: e.target.value })} required /> <textarea placeholder="Excerpt" value={formData.excerpt} onChange={(e) => setFormData({ ...formData, excerpt: e.target.value })} /> <textarea placeholder="Content (Markdown)" value={formData.content} onChange={(e) => setFormData({ ...formData, content: e.target.value })} rows={15} required /> <input type="text" placeholder="Tags (comma separated)" value={formData.tags} onChange={(e) => setFormData({ ...formData, tags: e.target.value })} /> <button type="submit" disabled={loading}> {loading ? 'Creating...' : 'Create Post'} </button> {error && <div className="error">{error.message}</div>} </form> ); } export default CreatePost;
Cache Updates: Use `refetchQueries` to automatically update the cache after mutations, or manually update the cache with `update` function for optimistic UI updates.

Real-Time Updates with Subscriptions

// src/graphql/subscriptions.js import { gql } from '@apollo/client'; export const POST_CREATED_SUBSCRIPTION = gql` subscription OnPostCreated { postCreated { id title slug excerpt author { username } } } `; export const COMMENT_ADDED_SUBSCRIPTION = gql` subscription OnCommentAdded($postId: ID!) { commentAdded(postId: $postId) { id content createdAt author { id username avatar } } } `;
// src/components/PostDetail.jsx import React, { useEffect } from 'react'; import { useQuery, useSubscription } from '@apollo/client'; import { useParams } from 'react-router-dom'; import { GET_POST } from '../graphql/queries'; import { COMMENT_ADDED_SUBSCRIPTION } from '../graphql/subscriptions'; function PostDetail() { const { slug } = useParams(); const { loading, error, data } = useQuery(GET_POST, { variables: { slug }, }); // Subscribe to new comments useSubscription(COMMENT_ADDED_SUBSCRIPTION, { variables: { postId: data?.post?.id }, skip: !data?.post?.id, onData: ({ client, data: subData }) => { // Update cache with new comment const newComment = subData.data.commentAdded; client.cache.modify({ id: client.cache.identify(data.post), fields: { comments(existingComments = []) { return [...existingComments, newComment]; }, }, }); }, }); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; const { post } = data; return ( <article className="post-detail"> <h1>{post.title}</h1> {post.featuredImage && ( <img src={post.featuredImage} alt={post.title} /> )} <div className="post-meta"> <span>By {post.author.username}</span> <span>{new Date(post.createdAt).toLocaleDateString()}</span> <span>{post.viewCount} views</span> </div> <div className="post-content" dangerouslySetInnerHTML={{ __html: post.content }} /> <div className="post-tags"> {post.tags.map(tag => ( <span key={tag} className="tag">{tag}</span> ))} </div> <CommentsSection comments={post.comments} postId={post.id} /> </article> ); } export default PostDetail;

Error Handling and Loading States

// src/components/ErrorBoundary.jsx import React from 'react'; import { useQuery } from '@apollo/client'; function ErrorDisplay({ error }) { if (error.networkError) { return ( <div className="error-network"> <h3>Network Error</h3> <p>Unable to connect to the server. Please check your connection.</p> </div> ); } if (error.graphQLErrors) { return ( <div className="error-graphql"> <h3>Error</h3> {error.graphQLErrors.map((err, i) => ( <p key={i}>{err.message}</p> ))} </div> ); } return ( <div className="error-unknown"> <h3>An error occurred</h3> <p>{error.message}</p> </div> ); } // Loading component function LoadingSpinner() { return ( <div className="loading-spinner"> <div className="spinner"></div> <p>Loading...</p> </div> ); } export { ErrorDisplay, LoadingSpinner };

Client-Side Caching Strategy

// Optimistic UI updates const [deletePost] = useMutation(DELETE_POST, { optimisticResponse: { deletePost: true, }, update: (cache, { data }) => { if (data.deletePost) { cache.modify({ fields: { posts(existingPosts = [], { readField }) { return existingPosts.filter( postRef => readField('id', postRef) !== postId ); }, }, }); } }, }); // Cache persistence import { persistCache } from 'apollo3-cache-persist'; const cache = new InMemoryCache(); await persistCache({ cache, storage: window.localStorage, });
Cache Consistency: Always invalidate or update cache after mutations to keep UI in sync. Use optimistic responses for instant user feedback before server confirmation.
Practice Exercise:
  1. Implement authentication flow (login/register/logout)
  2. Create a comment form with real-time updates
  3. Add infinite scroll pagination for posts
  4. Implement file upload for featured images
  5. Add error handling and retry logic
  6. Test real-time subscriptions with multiple browser tabs
Production Ready: Your React + Apollo Client app now has complete GraphQL integration with queries, mutations, subscriptions, caching, and error handling. Ready to deploy!