Why GraphQL and Rork Are a Natural Pair
REST APIs have long been the standard for mobile development, but GraphQL addresses their core limitations with a fundamentally different philosophy. The ability to fetch only the fields you need, combine multiple resources in a single request, and use real-time subscriptions as a first-class feature makes GraphQL an excellent fit for data-intensive Rork applications.
This walkthrough integrates Apollo Client into a Rork app and connects it to a GraphQL backend, moving from environment setup and schema design through queries, mutations, subscriptions, and production-ready caching — each step backed by code you can run.
What you'll learn:
- Setting up Apollo Client and configuring the provider
- Implementing type-safe queries and mutations
- Building real-time subscriptions over WebSocket
- Optimizing performance with InMemoryCache
- Handling errors and loading states gracefully
Target audience: Mid-to-advanced developers comfortable with React Native and Rork who want to level up their API architecture.
Prerequisites and Setup
What You'll Need
- Rork Max (recommended) or Rork Pro
- Node.js 18 or later
- A GraphQL backend (this guide uses Hasura as an example)
Installing the Packages
Use Rork's AI chat to add the required packages to your project:
# Apollo Client core packages
npm install @apollo/client graphql
# WebSocket support for subscriptions
npm install graphql-ws
# Type generation tools (recommended)
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apolloSetting Up the GraphQL Client
Initializing Apollo Client
Create a dedicated file for your Apollo Client instance:
// lib/apolloClient.ts
import { ApolloClient, InMemoryCache, createHttpLink, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';
// HTTP link for queries and mutations
const httpLink = createHttpLink({
uri: 'https://your-hasura-instance.hasura.app/v1/graphql',
headers: {
'x-hasura-admin-secret': process.env.EXPO_PUBLIC_HASURA_SECRET || '',
},
});
// WebSocket link for subscriptions
const wsLink = new GraphQLWsLink(
createClient({
url: 'wss://your-hasura-instance.hasura.app/v1/graphql',
connectionParams: {
headers: {
'x-hasura-admin-secret': process.env.EXPO_PUBLIC_HASURA_SECRET || '',
},
},
})
);
// Route operations to the right link automatically
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink, // Subscriptions → WebSocket
httpLink // Queries & mutations → HTTP
);
// Cache configuration
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
todos: {
// Pagination merge strategy
keyArgs: ['where'],
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
});
export const client = new ApolloClient({
link: splitLink,
cache,
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network', // Show cached data, refresh in background
},
},
});Wrapping Your App with the Provider
In app/_layout.tsx (App Router), wrap your app with ApolloProvider:
// app/_layout.tsx
import { ApolloProvider } from '@apollo/client';
import { client } from '../lib/apolloClient';
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<ApolloProvider client={client}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
</ApolloProvider>
);
}Implementing Queries — Fetching Data
Basic Query with useQuery
Here's a clean, reusable hook for fetching todos:
// hooks/useTodos.ts
import { gql, useQuery } from '@apollo/client';
const GET_TODOS = gql`
query GetTodos($userId: uuid!) {
todos(where: { user_id: { _eq: $userId } }, order_by: { created_at: desc }) {
id
title
completed
created_at
priority
}
}
`;
export type Todo = {
id: string;
title: string;
completed: boolean;
created_at: string;
priority: 'low' | 'medium' | 'high';
};
export function useTodos(userId: string) {
const { data, loading, error, refetch } = useQuery<{ todos: Todo[] }>(
GET_TODOS,
{
variables: { userId },
skip: !userId,
}
);
return {
todos: data?.todos ?? [],
loading,
error,
refetch,
};
}Using It in a Component
// components/TodoList.tsx
import React from 'react';
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';
import { useTodos } from '../hooks/useTodos';
type Props = {
userId: string;
};
export function TodoList({ userId }: Props) {
const { todos, loading, error } = useTodos(userId);
if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color="#6366f1" />
<Text style={styles.loadingText}>Loading your todos...</Text>
</View>
);
}
if (error) {
return (
<View style={styles.center}>
<Text style={styles.errorText}>
Something went wrong: {error.message}
</Text>
</View>
);
}
return (
<FlatList
data={todos}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.todoItem}>
<Text style={[styles.title, item.completed && styles.completed]}>
{item.title}
</Text>
<View style={[styles.priority, styles[`priority_${item.priority}`]]}>
<Text style={styles.priorityText}>{item.priority}</Text>
</View>
</View>
)}
/>
);
}Implementing Mutations — Updating Data
Optimistic UI for a Snappy Experience
Optimistic updates let your UI respond instantly, before the server confirms the change:
// hooks/useTodoMutations.ts
import { gql, useMutation } from '@apollo/client';
const ADD_TODO = gql`
mutation AddTodo($title: String!, $userId: uuid!, $priority: String!) {
insert_todos_one(
object: { title: $title, user_id: $userId, priority: $priority, completed: false }
) {
id
title
completed
created_at
priority
}
}
`;
const TOGGLE_TODO = gql`
mutation ToggleTodo($id: uuid!, $completed: Boolean!) {
update_todos_by_pk(
pk_columns: { id: $id }
_set: { completed: $completed }
) {
id
completed
}
}
`;
const DELETE_TODO = gql`
mutation DeleteTodo($id: uuid!) {
delete_todos_by_pk(id: $id) {
id
}
}
`;
export function useTodoMutations(userId: string) {
const [addTodo, { loading: addLoading }] = useMutation(ADD_TODO, {
// Optimistic response: update UI before server confirms
optimisticResponse: ({ title, priority }) => ({
insert_todos_one: {
__typename: 'todos',
id: `temp-${Date.now()}`, // Temporary ID, replaced after server responds
title,
completed: false,
created_at: new Date().toISOString(),
priority,
},
}),
update(cache, { data }) {
cache.modify({
fields: {
todos(existingTodos = []) {
const newTodoRef = cache.writeFragment({
data: data?.insert_todos_one,
fragment: gql`
fragment NewTodo on todos {
id
title
completed
created_at
priority
}
`,
});
return [newTodoRef, ...existingTodos];
},
},
});
},
});
const [toggleTodo] = useMutation(TOGGLE_TODO);
const [deleteTodo] = useMutation(DELETE_TODO, {
// Remove from cache after deletion
update(cache, { data }) {
const deletedId = data?.delete_todos_by_pk?.id;
cache.evict({ id: cache.identify({ __typename: 'todos', id: deletedId }) });
cache.gc();
},
});
return {
addTodo: (title: string, priority: string) =>
addTodo({ variables: { title, userId, priority } }),
toggleTodo: (id: string, completed: boolean) =>
toggleTodo({ variables: { id, completed } }),
deleteTodo: (id: string) => deleteTodo({ variables: { id } }),
addLoading,
};
}Subscriptions — Real-Time Data Updates
Subscriptions are where GraphQL really shines. They're essential for chat apps, collaborative editors, live dashboards, and anything that needs to push updates to clients instantly:
// hooks/useRealtimeTodos.ts
import { gql, useSubscription } from '@apollo/client';
const TODOS_SUBSCRIPTION = gql`
subscription WatchTodos($userId: uuid!) {
todos(
where: { user_id: { _eq: $userId } }
order_by: { created_at: desc }
) {
id
title
completed
created_at
priority
}
}
`;
export function useRealtimeTodos(userId: string) {
const { data, loading, error } = useSubscription(
TODOS_SUBSCRIPTION,
{
variables: { userId },
skip: !userId,
onSubscriptionData: ({ subscriptionData }) => {
console.log('Real-time update received:', subscriptionData.data?.todos?.length, 'items');
},
}
);
return {
todos: data?.todos ?? [],
loading,
error,
};
}Expected behavior: Any time a todo is added, updated, or deleted on the server, all connected clients will receive the update instantly — no polling or manual
refetchrequired while the WebSocket connection is alive.
Cache Optimization — Maximizing Performance
Fine-tuning Apollo's InMemoryCache reduces network requests and creates a noticeably snappier experience:
// lib/apolloClient.ts (detailed cache config)
const cache = new InMemoryCache({
typePolicies: {
todos: {
keyFields: ['id'],
},
Query: {
fields: {
// Offset-based pagination support
todos: {
keyArgs: ['where', 'order_by'],
merge(existing, incoming, { args }) {
const merged = existing ? existing.slice(0) : [];
if (args?.offset !== undefined) {
for (let i = 0; i < incoming.length; ++i) {
merged[args.offset + i] = incoming[i];
}
} else {
merged.push.apply(merged, incoming);
}
return merged;
},
read(existing, { args }) {
if (!existing) return existing;
const { offset = 0, limit = existing.length } = args ?? {};
return existing.slice(offset, offset + limit);
},
},
},
},
},
});Choosing the Right fetchPolicy
| fetchPolicy | Best for | Behavior |
|---|---|---|
cache-first (default) | Static/reference data | Serves from cache; skips network if data exists |
cache-and-network | Dashboards, feeds | Shows cached data immediately, updates in background |
network-only | Payments, auth | Always fetches fresh data |
no-cache | Sensitive data | Never stores in cache |
cache-only | Offline mode | Only reads from cache |
Common Errors and How to Fix Them
"Network error: Failed to fetch"
→ Double-check your endpoint URL and auth headers. When using the Android emulator with a local server, use http://10.0.2.2:4000/graphql instead of localhost.
"Cache data may be lost when replacing the todos field" warning
→ Define a merge function in typePolicies for the affected field. See the cache configuration example above.
Subscription not connecting
→ Make sure your WebSocket URL uses wss:// (TLS) in production. In development, ws:// is fine, but production always needs TLS for security.
"Cannot query field X on type Y" error → Your query's field names don't match the schema. Open Hasura's built-in GraphiQL explorer to inspect your schema and correct the field names.
Advanced: Integrating Authentication
Here's how to attach a JWT token to every request using an auth link:
// lib/apolloClient.ts (auth-integrated version)
import { setContext } from '@apollo/client/link/context';
import * as SecureStore from 'expo-secure-store';
const authLink = setContext(async (_, { headers }) => {
const token = await SecureStore.getItemAsync('auth_token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
// Chain: authLink → httpLink
const authenticatedHttpLink = authLink.concat(httpLink);For a deep dive into authentication setup, check out our Rork × Firebase/Supabase Authentication Guide.
To see GraphQL and Supabase working together in a real-time chat app, visit Building a Supabase Chat App with Rork.
For managing app-wide state alongside Apollo's cache, the Rork App State Management Patterns Guide pairs well with this article.
Wrapping Up
In this guide, we walked through a complete implementation of Apollo Client and GraphQL in a Rork app — from initial setup through real-time subscriptions and production cache tuning.
GraphQL has a steeper learning curve than REST, but once it's in place, it makes your data layer dramatically more flexible. You only fetch what you need, real-time updates become straightforward, and the Apollo Client cache keeps your UI feeling instant even on slower connections.
Use the code examples in this article as a starting point, and adapt them to your specific data model. Whether you're building a collaborative tool, a live dashboard, or a complex social app, GraphQL and Apollo Client are a combination well worth mastering.