RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-03-24Advanced

Rork × GraphQL Complete Implementation Guide — Building Real-Time Apps with Apollo Client

A complete guide to integrating GraphQL and Apollo Client into your Rork apps. From schema design to queries, mutations, subscriptions, and cache optimization — with practical code examples throughout.

GraphQLApollo ClientRork515React Native209Real-TimeAPI IntegrationAdvanced4

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-apollo

Setting 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 refetch required 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

fetchPolicyBest forBehavior
cache-first (default)Static/reference dataServes from cache; skips network if data exists
cache-and-networkDashboards, feedsShows cached data immediately, updates in background
network-onlyPayments, authAlways fetches fresh data
no-cacheSensitive dataNever stores in cache
cache-onlyOffline modeOnly 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.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-07-15
The Comma That Fell to the Start of a Line: Rork, Quote Cards, and Japanese Line Breaking
React Native's Text component does not guarantee Japanese line-breaking rules. Here are the violation rates I measured, a WORD JOINER implementation that survives copy and VoiceOver, an onTextLayout audit harness, and how Rork Max (SwiftUI) differs.
Dev Tools2026-07-15
My Rork sleep timer faded out on time — but the sound didn't
Rork writes sleep-timer fades against the wall clock with setInterval. The numbers say 30 minutes, but the real audio fades early or cuts out abruptly. Here is how to drive the fade from the actual playback position, why a logarithmic curve sounds natural, and the honest limit of JS fades when the screen is locked.
Dev Tools2026-07-14
Designing Seams That Survive AI Regeneration in Rork
Every follow-up prompt to Rork can quietly wipe out logic you wrote by hand. Protecting it with prompts is a patch, not a fix. Here is how to separate generated code from code you own, and draw a boundary that regeneration cannot reach, with working Zustand and service-layer examples.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →