Skip to content

title: React Native with Apollo Client for FraiseQL description: Complete guide for querying FraiseQL servers from React Native mobile applications. keywords: ["debugging", "implementation", "best-practices", "deployment", "tutorial"] tags: ["documentation", "reference"]


React Native with Apollo Client for FraiseQL

Status: ✅ Production Ready Audience: React Native developers Reading Time: 25-30 minutes Last Updated: 2026-02-05

Complete guide for querying FraiseQL servers from React Native mobile applications.


Installation & Setup

Prerequisites

  • React Native 0.68+
  • Node.js 16+
  • FraiseQL server running
  • iOS: Xcode 14+
  • Android: Android Studio, API level 21+

Add Dependencies

npm install @apollo/client graphql subscriptions-transport-ws
# or
yarn add @apollo/client graphql subscriptions-transport-ws

For async storage:

npm install @react-native-async-storage/async-storage
# Link pod (iOS)
npx pod-install ios

Configure Apollo Client

// apolloClient.ts
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';
import AsyncStorage from '@react-native-async-storage/async-storage';

// Error handling
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      console.error(`[GraphQL error]: ${message}`, { locations, path });
    });
  }

  if (networkError) {
    if ('statusCode' in networkError && networkError.statusCode === 401) {
      // Handle authentication error - log out user
      AsyncStorage.removeItem('token');
      // Navigation to login screen would go here
    }
    console.error(`[Network error]: ${networkError}`);
  }

  return forward(operation);
});

// HTTP link for queries and mutations
const httpLink = new HttpLink({
  uri: 'http://localhost:8000/graphql',
  fetchOptions: {
    timeout: 10000, // 10 second timeout for mobile networks
  },
  request: async (operation) => {
    const token = await AsyncStorage.getItem('token');
    if (token) {
      operation.setContext((previousContext) => ({
        ...previousContext,
        headers: {
          authorization: `Bearer ${token}`,
        },
      }));
    }
  },
});

// WebSocket link for subscriptions
const wsLink = new WebSocketLink({
  uri: 'ws://localhost:8000/graphql',
  options: {
    reconnect: true,
    inactivityTimeout: 30000,
    connectionParams: async () => ({
      authorization: `Bearer ${await AsyncStorage.getItem('token')}`,
    }),
  },
});

// Split between WebSocket (subscriptions) and HTTP (queries/mutations)
const splitLink = from([
  errorLink,
  split(
    ({ query }) => {
      const definition = getMainDefinition(query);
      return (
        definition.kind === 'OperationDefinition' &&
        definition.operation === 'subscription'
      );
    },
    wsLink,
    httpLink
  ),
]);

// Persistent cache using AsyncStorage
const cache = new InMemoryCache({
  // Optional: add persistent cache layer
});

export const apolloClient = new ApolloClient({
  link: splitLink,
  cache,
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network',
    },
  },
});

Setup in App

// App.tsx
import React from 'react';
import { ApolloProvider } from '@apollo/client';
import { apolloClient } from './apolloClient';
import RootNavigator from './navigation/RootNavigator';

export default function App() {
  return (
    <ApolloProvider client={apolloClient}>
      <RootNavigator />
    </ApolloProvider>
  );
}

Queries

Basic Query Hook

import { useQuery, gql } from '@apollo/client';
import { View, Text, ActivityIndicator, FlatList } from 'react-native';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

interface User {
  id: string;
  name: string;
  email: string;
}

export function UserListScreen() {
  const { data, loading, error } = useQuery<{ users: User[] }>(GET_USERS);

  if (loading) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  if (error) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <Text>Error: {error.message}</Text>
      </View>
    );
  }

  return (
    <FlatList
      data={data?.users}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={{ padding: 16, borderBottomWidth: 1 }}>
          <Text style={{ fontSize: 16, fontWeight: 'bold' }}>{item.name}</Text>
          <Text style={{ color: '#666' }}>{item.email}</Text>
        </View>
      )}
    />
  );
}

Query with Variables

const GET_USER_BY_ID = gql`
  query GetUserById($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

interface UserDetailScreenProps {
  route: { params: { userId: string } };
}

export function UserDetailScreen({ route }: UserDetailScreenProps) {
  const { userId } = route.params;
  const { data, loading, error } = useQuery(GET_USER_BY_ID, {
    variables: { id: userId },
  });

  if (loading) return <ActivityIndicator />;
  if (error) return <Text>Error: {error.message}</Text>;

  const user = data?.user;

  return (
    <ScrollView style={{ flex: 1, padding: 16 }}>
      <Text style={{ fontSize: 24, fontWeight: 'bold' }}>{user?.name}</Text>
      <Text style={{ color: '#666', marginBottom: 16 }}>{user?.email}</Text>

      <Text style={{ fontSize: 18, fontWeight: 'bold', marginTop: 16 }}>
        Posts
      </Text>
      {user?.posts?.map((post: any) => (
        <View key={post.id} style={{ marginTop: 8, paddingLeft: 8 }}>
          <Text>{post.title}</Text>
        </View>
      ))}
    </ScrollView>
  );
}

Pagination for Mobile

export function PostListScreen() {
  const pageSize = 20;
  const [page, setPage] = useState(0);

  const { data, loading, error, fetchMore } = useQuery(
    GET_POSTS_PAGINATED,
    {
      variables: { limit: pageSize, offset: 0 },
    }
  );

  const handleLoadMore = () => {
    fetchMore({
      variables: {
        offset: (page + 1) * pageSize,
      },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) return prev;
        setPage(page + 1);
        return {
          posts: [...prev.posts, ...fetchMoreResult.posts],
          postsCount: fetchMoreResult.postsCount,
        };
      },
    });
  };

  if (loading && page === 0) {
    return <ActivityIndicator />;
  }

  return (
    <FlatList
      data={data?.posts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={{ padding: 16, borderBottomWidth: 1 }}>
          <Text style={{ fontSize: 16 }}>{item.title}</Text>
          <Text style={{ color: '#999', fontSize: 12 }}>
            {new Date(item.createdAt).toLocaleDateString()}
          </Text>
        </View>
      )}
      onEndReached={handleLoadMore}
      onEndReachedThreshold={0.8}
      ListFooterComponent={
        loading && page > 0 ? <ActivityIndicator /> : null
      }
    />
  );
}

Mutations

Basic Mutation

import { useMutation, gql } from '@apollo/client';
import { View, TextInput, TouchableOpacity, Text } from 'react-native';
import { useState } from 'react';

const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!) {
    createPost(title: $title, content: $content) {
      id
      title
      content
      createdAt
    }
  }
`;

export function CreatePostScreen() {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');

  const [createPost, { loading, error }] = useMutation(CREATE_POST);

  const handleCreate = async () => {
    try {
      const result = await createPost({
        variables: { title, content },
      });
      console.log('Post created:', result.data);
      setTitle('');
      setContent('');
      // Navigate back or show success
    } catch (err) {
      console.error('Error:', err);
    }
  };

  return (
    <View style={{ flex: 1, padding: 16 }}>
      <TextInput
        value={title}
        onChangeText={setTitle}
        placeholder="Post title"
        style={{ borderBottomWidth: 1, marginBottom: 16, padding: 8 }}
      />
      <TextInput
        value={content}
        onChangeText={setContent}
        placeholder="Post content"
        multiline
        numberOfLines={6}
        style={{
          borderBottomWidth: 1,
          marginBottom: 16,
          padding: 8,
          textAlignVertical: 'top',
        }}
      />
      <TouchableOpacity
        onPress={handleCreate}
        disabled={loading}
        style={{
          backgroundColor: loading ? '#ccc' : '#007AFF',
          padding: 12,
          borderRadius: 8,
          alignItems: 'center',
        }}
      >
        <Text style={{ color: 'white', fontWeight: 'bold' }}>
          {loading ? 'Creating...' : 'Create Post'}
        </Text>
      </TouchableOpacity>
      {error && <Text style={{ color: 'red', marginTop: 16 }}>{error.message}</Text>}
    </View>
  );
}

Optimistic Updates

const DELETE_POST = gql`
  mutation DeletePost($id: ID!) {
    deletePost(id: $id) {
      id
    }
  }
`;

export function PostCard({ post, onDelete }: any) {
  const [deletePost] = useMutation(DELETE_POST, {
    optimisticResponse: {
      deletePost: { id: post.id, __typename: 'Post' },
    },
    update(cache, { data }) {
      cache.evict({ id: cache.identify(post) });
    },
  });

  return (
    <View style={{ padding: 16, borderBottomWidth: 1 }}>
      <Text style={{ fontSize: 16 }}>{post.title}</Text>
      <TouchableOpacity
        onPress={() => deletePost({ variables: { id: post.id } })}
        style={{ marginTop: 8, padding: 8 }}
      >
        <Text style={{ color: '#FF3B30' }}>Delete</Text>
      </TouchableOpacity>
    </View>
  );
}

Subscriptions

Real-Time Updates

import { useSubscription, gql } from '@apollo/client';

const ON_POST_CREATED = gql`
  subscription OnPostCreated {
    postCreated {
      id
      title
      author {
        name
      }
    }
  }
`;

export function PostFeedScreen() {
  const [posts, setPosts] = useState<any[]>([]);
  const { data, loading, error } = useSubscription(ON_POST_CREATED);

  useEffect(() => {
    if (data?.postCreated) {
      setPosts((prev) => [data.postCreated, ...prev]);
    }
  }, [data?.postCreated]);

  if (loading && posts.length === 0) {
    return <ActivityIndicator />;
  }

  return (
    <FlatList
      data={posts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={{ padding: 16, borderBottomWidth: 1 }}>
          <Text style={{ fontWeight: 'bold' }}>{item.title}</Text>
          <Text style={{ color: '#666', fontSize: 12 }}>
            by {item.author.name}
          </Text>
        </View>
      )}
      ListHeaderComponent={
        error ? (
          <Text style={{ color: 'red', padding: 16 }}>
            Error: {error.message}
          </Text>
        ) : null
      }
    />
  );
}

Error Handling

Network Error Recovery

export function useNetworkAwareQuery<T>(document: any, options?: any) {
  const { data, loading, error, refetch } = useQuery<T>(document, {
    ...options,
    errorPolicy: 'all',
    notifyOnNetworkStatusChange: true,
  });

  const [isOffline, setIsOffline] = useState(false);

  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener((state) => {
      setIsOffline(!state.isConnected);
      // Automatically retry when connection is restored
      if (state.isConnected && error) {
        refetch();
      }
    });

    return () => unsubscribe();
  }, [error, refetch]);

  return { data, loading, error, refetch, isOffline };
}

// Usage
export function ResilientUserList() {
  const { data, loading, error, isOffline } = useNetworkAwareQuery(GET_USERS);

  if (isOffline) {
    return (
      <View style={{ flex: 1, justifyContent: 'center' }}>
        <Text style={{ textAlign: 'center' }}>No internet connection</Text>
      </View>
    );
  }

  if (error) {
    return (
      <Text style={{ color: 'red', padding: 16 }}>
        Error: {error.message}
      </Text>
    );
  }

  // ... render list
}

Offline Support

Persist Cache to AsyncStorage

import { persistCache } from 'apollo3-cache-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';

async function createClient() {
  const cache = new InMemoryCache();

  await persistCache({
    cache,
    storage: AsyncStorage,
  });

  return new ApolloClient({
    cache,
    link: httpLink,
  });
}

Queue Mutations While Offline

export function useOfflineAwareMutation(document: any) {
  const [isOffline, setIsOffline] = useState(false);
  const [queuedMutations, setQueuedMutations] = useState<any[]>([]);

  const [mutate, result] = useMutation(document);

  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener((state) => {
      const offline = !state.isConnected;
      setIsOffline(offline);

      // Process queued mutations when connection is restored
      if (!offline && queuedMutations.length > 0) {
        queuedMutations.forEach(async (mutation) => {
          await mutate({ variables: mutation });
        });
        setQueuedMutations([]);
      }
    });

    return () => unsubscribe();
  }, [queuedMutations, mutate]);

  const offlineAwareMutate = async (variables: any) => {
    if (isOffline) {
      setQueuedMutations((prev) => [...prev, variables]);
      console.log('Mutation queued for when connection is restored');
    } else {
      await mutate({ variables });
    }
  };

  return [offlineAwareMutate, result, queuedMutations] as const;
}

Testing

Mock Apollo Client for Tests

import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react-native';

const mocks = [
  {
    request: {
      query: GET_USERS,
    },
    result: {
      data: {
        users: [
          { id: '1', name: 'Alice', email: 'alice@example.com' },
        ],
      },
    },
  },
];

describe('UserListScreen', () => {
  it('displays users', async () => {
    const { findByText } = render(
      <MockedProvider mocks={mocks}>
        <UserListScreen />
      </MockedProvider>
    );

    const alice = await findByText('Alice');
    expect(alice).toBeTruthy();
  });
});

Platform-Specific Considerations

iOS Specific

import { Platform } from 'react-native';

// Use NSURLSession for iOS (better for mobile networks)
const httpLink = new HttpLink({
  uri: 'http://localhost:8000/graphql',
  credentials: 'include',
  // iOS automatically handles network requests efficiently
});

Android Specific

// Handle network changes on Android
import { Platform } from 'react-native';

const wsLink = new WebSocketLink({
  uri: 'ws://localhost:8000/graphql',
  options: {
    reconnect: true,
    // Android may kill background connections
    inactivityTimeout: Platform.OS === 'android' ? 45000 : 30000,
  },
});

Deploy to App Stores

iOS App Store

# Build for App Store
eas build --platform ios --auto-submit

# Or use Xcode
open ios/YourApp.xcworkspace
# Archive and Upload using Xcode's organizer

Android Play Store

# Build for Play Store
eas build --platform android --auto-submit

# Or build locally
cd android && ./gradlew bundleRelease
# Upload to Play Console

See Also

Related Guides:

React Native & Apollo Documentation: