title: React with Apollo Client for FraiseQL description: Complete guide for querying FraiseQL servers from React applications using Apollo Client. keywords: ["debugging", "implementation", "best-practices", "deployment", "tutorial"] tags: ["documentation", "reference"]
React with Apollo Client for FraiseQL¶
Status: ✅ Production Ready Audience: React developers Reading Time: 25-30 minutes Last Updated: 2026-02-05
Complete guide for querying FraiseQL servers from React applications using Apollo Client.
Installation & Setup¶
Prerequisites¶
- React 16.8+ (hooks support)
- Node.js 16+
- FraiseQL server running (see Full-Stack Examples)
Install Dependencies¶
npm install @apollo/client graphql subscriptions-transport-ws
Or with Yarn:
yarn add @apollo/client graphql subscriptions-transport-ws
Initialize Apollo Client¶
import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';
// Error handling link
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) =>
console.error(`[GraphQL error]: Message: ${message}, Path: ${path}`, locations)
);
}
if (networkError) console.error(`[Network error]: ${networkError}`);
});
// HTTP link for queries/mutations
const httpLink = new HttpLink({
uri: 'http://localhost:8000/graphql',
credentials: 'include', // Send cookies for authentication
});
// WebSocket link for subscriptions
const wsLink = new WebSocketLink({
uri: 'ws://localhost:8000/graphql',
options: {
reconnect: true,
connectionParams: () => ({
authorization: localStorage.getItem('token') || '',
}),
},
});
// Choose link based on operation type
const splitLink = from([
errorLink,
new (typeof window !== 'undefined' ? require('@apollo/client').split : null)?.(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
) || httpLink,
]);
// Create Apollo Client
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
});
export default client;
Wrap App with ApolloProvider¶
import { ApolloProvider } from '@apollo/client';
import client from './apolloClient';
import App from './App';
function Root() {
return (
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
}
export default Root;
Queries¶
Basic Query Hook¶
import { useQuery, gql } from '@apollo/client';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
export function UserList() {
const { data, loading, error } = useQuery(GET_USERS);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data?.users?.map((user) => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
);
}
Query with Variables¶
import { useQuery, gql } from '@apollo/client';
const GET_USER_BY_ID = gql`
query GetUserById($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
`;
interface UserDetailProps {
userId: string;
}
export function UserDetail({ userId }: UserDetailProps) {
const { data, loading, error } = useQuery(GET_USER_BY_ID, {
variables: { id: userId },
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data?.user?.name}</h1>
<p>Email: {data?.user?.email}</p>
<h2>Posts:</h2>
<ul>
{data?.user?.posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Pagination Pattern¶
const GET_POSTS_PAGINATED = gql`
query GetPostsPaginated($limit: Int!, $offset: Int!) {
posts(limit: $limit, offset: $offset) {
id
title
createdAt
}
postsCount
}
`;
export function PostList() {
const [page, setPage] = useState(1);
const pageSize = 10;
const { data, loading, error, fetchMore } = useQuery(
GET_POSTS_PAGINATED,
{
variables: { limit: pageSize, offset: 0 },
}
);
const handleLoadMore = () => {
fetchMore({
variables: {
offset: page * pageSize,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
posts: [...(prev.posts || []), ...(fetchMoreResult.posts || [])],
postsCount: fetchMoreResult.postsCount,
};
},
});
setPage(page + 1);
};
if (loading) return <div>Loading...</div>;
return (
<div>
<ul>
{data?.posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<button onClick={handleLoadMore}>Load More</button>
<p>Loaded {data?.posts?.length} of {data?.postsCount}</p>
</div>
);
}
Fetch Policies¶
// cache-first: Return from cache if available (default for most queries)
useQuery(GET_USERS, { fetchPolicy: 'cache-first' });
// cache-and-network: Return from cache, then refetch in background
useQuery(GET_USERS, { fetchPolicy: 'cache-and-network' });
// cache-only: Only use cache, never fetch
useQuery(GET_USERS, { fetchPolicy: 'cache-only' });
// network-only: Always fetch from server
useQuery(GET_USERS, { fetchPolicy: 'network-only' });
// no-cache: Don't use cache at all
useQuery(GET_USERS, { fetchPolicy: 'no-cache' });
Mutations¶
Basic Mutation Hook¶
import { useMutation, gql } from '@apollo/client';
const CREATE_POST = gql`
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
createdAt
}
}
`;
export function CreatePostForm() {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [createPost, { loading, error }] = useMutation(CREATE_POST);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const result = await createPost({
variables: { title, content },
});
console.log('Post created:', result.data?.createPost);
setTitle('');
setContent('');
} catch (err) {
console.error('Error creating post:', err);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post title"
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Post content"
/>
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Post'}
</button>
{error && <div className="error">{error.message}</div>}
</form>
);
}
Update Cache After Mutation¶
const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $name: String!) {
updateUser(id: $id, name: $name) {
id
name
email
}
}
`;
export function EditUserName({ userId, currentName }: any) {
const [newName, setNewName] = useState(currentName);
const [updateUser] = useMutation(UPDATE_USER, {
update(cache, { data: { updateUser } }) {
// Update cache directly
cache.modify({
fields: {
users(existingUsers = []) {
return existingUsers.map((user: any) =>
user.id === updateUser.id ? updateUser : user
);
},
},
});
},
});
const handleSave = async () => {
await updateUser({
variables: { id: userId, name: newName },
});
};
return (
<div>
<input
value={newName}
onChange={(e) => setNewName(e.target.value)}
/>
<button onClick={handleSave}>Save</button>
</div>
);
}
Optimistic Response¶
const DELETE_POST = gql`
mutation DeletePost($id: ID!) {
deletePost(id: $id) {
id
}
}
`;
export function PostActions({ post }: any) {
const [deletePost] = useMutation(DELETE_POST, {
optimisticResponse: {
deletePost: { id: post.id, __typename: 'Post' },
},
update(cache, { data }) {
cache.evict({ id: cache.identify(post) });
},
});
return (
<button
onClick={() =>
deletePost({ variables: { id: post.id } })
}
>
Delete
</button>
);
}
Subscriptions¶
WebSocket Setup (Already done in Apollo Client initialization)¶
Subscribe to Events¶
import { useSubscription, gql } from '@apollo/client';
const ON_POST_CREATED = gql`
subscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}
`;
export function PostFeed() {
const { data, loading, error } = useSubscription(ON_POST_CREATED);
if (loading) return <div>Connecting...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>New post created:</h2>
{data?.postCreated && (
<div>
<h3>{data.postCreated.title}</h3>
<p>by {data.postCreated.author.name}</p>
</div>
)}
</div>
);
}
Handle Subscription Lifecycle¶
const ON_MESSAGE = gql`
subscription OnMessage {
messageReceived {
id
text
user { name }
}
}
`;
export function ChatRoom() {
const [messages, setMessages] = useState<any[]>([]);
const { data, loading, error } = useSubscription(ON_MESSAGE);
useEffect(() => {
if (data?.messageReceived) {
setMessages((prev) => [...prev, data.messageReceived]);
}
}, [data?.messageReceived]);
return (
<div>
<div className="messages">
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.user.name}:</strong> {msg.text}
</div>
))}
</div>
</div>
);
}
Error Handling¶
Global Error Handler¶
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (const err of graphQLErrors) {
if (err.extensions?.code === 'UNAUTHENTICATED') {
// Handle auth error - redirect to login
localStorage.removeItem('token');
window.location.href = '/login';
}
if (err.extensions?.code === 'FORBIDDEN') {
console.error('Access denied:', err.message);
}
if (err.extensions?.code === 'VALIDATION_ERROR') {
console.error('Validation failed:', err.message);
}
}
}
if (networkError) {
if ('statusCode' in networkError && networkError.statusCode === 401) {
// Redirect to login
window.location.href = '/login';
}
if ('statusCode' in networkError && networkError.statusCode === 500) {
console.error('Server error. Please try again later.');
}
}
});
Component-Level Error Handling¶
export function SafeUserList() {
const { data, loading, error, refetch } = useQuery(GET_USERS);
if (error) {
return (
<div className="error-container">
<p>Failed to load users: {error.message}</p>
<button onClick={() => refetch()}>Try Again</button>
</div>
);
}
if (loading) return <div>Loading...</div>;
return (
<ul>
{data?.users?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Caching Strategies¶
Cache Management¶
import { useApolloClient } from '@apollo/client';
export function CacheManager() {
const client = useApolloClient();
const clearCache = () => {
client.cache.reset();
};
const getCache = (query: any, variables?: any) => {
return client.cache.readQuery({
query,
variables,
});
};
const updateCache = (query: any, variables: any, data: any) => {
client.cache.writeQuery({
query,
variables,
data,
});
};
return { clearCache, getCache, updateCache };
}
Custom Cache Policy per Component¶
export function UserDashboard() {
// Static user profile - use cache
const { data: profile } = useQuery(GET_PROFILE, {
fetchPolicy: 'cache-first',
});
// Real-time notifications - always fetch
const { data: notifications } = useQuery(GET_NOTIFICATIONS, {
fetchPolicy: 'network-only',
pollInterval: 5000, // Refetch every 5 seconds
});
// Activity feed - cache + refresh
const { data: activity } = useQuery(GET_ACTIVITY, {
fetchPolicy: 'cache-and-network',
});
return (
<div>
<Profile data={profile} />
<Notifications data={notifications} />
<Activity data={activity} />
</div>
);
}
Performance Optimization¶
Query Splitting¶
// ❌ Bad: Fetch everything at once
const DASHBOARD = gql`
query Dashboard {
currentUser { id name profile { bio } }
notifications { id message }
recentPosts { id title }
}
`;
// ✅ Good: Split based on render priority
const GET_USER = gql`query GetUser { currentUser { id name } }`;
const GET_NOTIFICATIONS = gql`query GetNotifications { notifications { id message } }`;
const GET_POSTS = gql`query GetPosts { recentPosts { id title } }`;
// Load user immediately, notifications and posts in background
export function Dashboard() {
const { data: user } = useQuery(GET_USER, { fetchPolicy: 'cache-first' });
const { data: notifications } = useQuery(GET_NOTIFICATIONS, { skip: !user });
const { data: posts } = useQuery(GET_POSTS, { skip: !user });
return (
<div>
<UserInfo data={user} />
<Suspense fallback="Loading...">
<Notifications data={notifications} />
<RecentPosts data={posts} />
</Suspense>
</div>
);
}
Request Batching¶
import { BatchHttpLink } from '@apollo/client/link/batch-http';
const batchLink = new BatchHttpLink({
uri: 'http://localhost:8000/graphql',
batchInterval: 10, // Batch queries sent within 10ms
batchMax: 5, // Maximum 5 queries per batch
});
Lazy Queries¶
import { useLazyQuery } from '@apollo/client';
export function SearchUsers() {
const [search, setSearch] = useState('');
const [executeSearch, { data, loading }] = useLazyQuery(SEARCH_USERS);
const handleSearch = (term: string) => {
setSearch(term);
if (term.length > 2) {
executeSearch({ variables: { term } });
}
};
return (
<div>
<input
value={search}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search users..."
/>
{loading && <div>Searching...</div>}
{data?.users?.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
Testing¶
Mock FraiseQL Responses¶
import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';
const mocks = [
{
request: {
query: GET_USERS,
},
result: {
data: {
users: [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
],
},
},
},
];
describe('UserList', () => {
it('renders user list', async () => {
render(
<MockedProvider mocks={mocks}>
<UserList />
</MockedProvider>
);
await screen.findByText('Alice (alice@example.com)');
expect(screen.getByText('Bob (bob@example.com)')).toBeInTheDocument();
});
});
Test Error Handling¶
const errorMocks = [
{
request: { query: GET_USERS },
error: new Error('Failed to fetch users'),
},
];
describe('UserList Error Handling', () => {
it('displays error message', async () => {
render(
<MockedProvider mocks={errorMocks}>
<UserList />
</MockedProvider>
);
await screen.findByText(/Error: Failed to fetch users/);
});
});
See Also¶
Related Guides:
- Full-Stack Python + React Example
- Real-Time Patterns - Subscription architecture
- Authentication & Authorization - Securing queries
- Production Deployment - React app hosting
Apollo Client Documentation: