RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
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 ClientRork548React Native234Real-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 $15 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-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
Dev Tools2026-08-14
Find the native edits expo prebuild will erase before you upgrade to SDK 57
Expo SDK 57 makes expo prebuild clear and regenerate ios and android by default. Here is how to audit your hand edits first, move them into config plugins, and why 57.0.9 matters for Reanimated apps.
Dev Tools2026-08-10
Switching to Signed URLs Killed My Image Cache — Decoupling expo-image Keys from the URL
Signed URLs rewrite their query string on every expiry, so a URL-keyed cache never hits. Here is how to derive a stable key and drive expo-image's writeToCacheAsync and readFromCacheAsync yourself, with measured results.
📚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 →