モバイルアプリ開発では、画面ごとに必要なデータが異なり、REST APIでは過剰取得(over-fetching)や取得不足(under-fetching)が頻発します。GraphQLは「必要なフィールドだけを1回のリクエストで取得する」設計思想を持ち、この問題を根本から解消します。

React NativeとGraphQLの組み合わせは、iOSとAndroidの両プラットフォームで統一的なデータ取得レイヤーを実現し、通信量の削減・開発速度の向上・型安全性の強化に直結します。

GraphQLがReact Nativeに適している理由

REST APIとの根本的な違い

REST APIでは、エンドポイントごとに返却されるデータ構造が固定されています。たとえばユーザープロフィール画面で「名前」と「アバターURL」だけが必要でも、/api/users/123は住所や電話番号など不要なフィールドも含めて返します。

GraphQLでは、クライアント側がクエリで必要なフィールドを明示的に指定します。

query UserProfile($id: ID!) {
  user(id: $id) {
    name
    avatarUrl
  }
}

このアプローチにより、モバイル回線でのデータ転送量を最小化できます。

モバイル開発特有の利点

課題REST APIでの対応GraphQLでの対応
画面ごとに必要なデータが異なるエンドポイントの乱立またはクエリパラメータで制御クエリで必要フィールドを指定
複数リソースの同時取得複数のAPI呼び出しが必要1つのクエリで複数リソースを結合
リアルタイム更新WebSocket・ポーリングを自前実装Subscriptionとして標準サポート
APIバージョニングURL・ヘッダーで管理スキーマの拡張で後方互換を維持
型安全性OpenAPI+コード生成が必要スキーマから直接TypeScript型を生成

React Native向けGraphQLクライアント比較

React NativeでGraphQLを利用する際、主要な選択肢は3つあります。プロジェクトの規模や要件に応じて最適なクライアントを選ぶ必要があります。

主要クライアント3種の特性

特性Apollo Client 4.0urql 5.xgraphql-request 7.x + TanStack Query
最新バージョン4.0(2025年8月リリース)5.0.17.4.0 + TanStack Query 5.x
バンドルサイズ約30KB(minified)約14KB(minified)約5KB + TanStack Query分
正規化キャッシュ標準搭載(InMemoryCache)Exchange追加で対応(@urql/exchange-graphcache)なし(TanStack Queryのクエリキャッシュ)
Subscription対応WebSocket・HTTP multipartExchange追加で対応別途実装が必要
React Native公式サポート公式ドキュメントあり公式対応制限なし
TypeScriptフル対応フル対応フル対応
DevtoolsReact Native Debugger連携urql DevtoolsTanStack Query Devtools
学習コスト高い中程度低い
npmダウンロード数/週約600万約20〜37万約520万(graphql-request単体)

選定の目安

  • Apollo Client: 大規模アプリ・正規化キャッシュが必須・Subscriptionを多用する場合
  • urql: 中規模アプリ・バンドルサイズを抑えたい・拡張性を重視する場合
  • graphql-request + TanStack Query: 小〜中規模アプリ・既にTanStack Queryを使用中・シンプルな構成を好む場合

Apollo Client 4.0によるセットアップ

Apollo Client 4.0は2025年8月にリリースされた最新メジャーバージョンです。RxJSがpeer dependencyとして追加され、バンドルサイズが20〜30%削減されました。

パッケージのインストール

npx expo install @apollo/client graphql rxjs

Apollo Client 4.0からはrxjsがpeer dependencyとなっているため、明示的なインストールが必要です。

基本セットアップ

src/lib/apollo.tsを作成し、クライアントインスタンスを初期化します。

import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";

const httpLink = new HttpLink({
  uri: "https://your-api.example.com/graphql",
});

export const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache(),
});

Apollo Client 4.0では、フレームワーク非依存のコア機能は@apollo/clientから、Reactフック(useQueryなど)は@apollo/client/reactからインポートする構成に整理されています。

ApolloProviderでアプリをラップ

App.tsxでプロバイダーを設定します。

import { ApolloProvider } from "@apollo/client/react";
import { client } from "./src/lib/apollo";

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

Queryの実装

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

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

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

interface GetUsersData {
  users: User[];
}

function UserList() {
  const { loading, error, data } = useQuery<GetUsersData>(GET_USERS);

  if (loading) return <ActivityIndicator size="large" />;
  if (error) return <Text>エラー: {error.message}</Text>;

  return (
    <FlatList
      data={data?.users}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={{ padding: 16 }}>
          <Text style={{ fontWeight: "bold" }}>{item.name}</Text>
          <Text>{item.email}</Text>
        </View>
      )}
    />
  );
}

Mutationの実装

import { useMutation, gql } from "@apollo/client/react";

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`;

function CreateUserForm() {
  const [createUser, { loading }] = useMutation(CREATE_USER, {
    refetchQueries: ["GetUsers"],
  });

  const handleSubmit = async (name: string, email: string) => {
    await createUser({
      variables: { name, email },
    });
  };

  // フォームUIの実装...
}

テキストストリーミング対応(@defer・Subscription over HTTP)

React Nativeの標準fetchはXHRベースでテキストストリーミングに対応していません。@deferディレクティブやHTTP multipart Subscriptionを使う場合は、ポリフィルの追加が必要です。

npx expo install react-native-fetch-api react-native-polyfill-globals

アプリのエントリーポイント(index.jsなど)の先頭にポリフィルを記述します。

import { polyfill as polyfillEncoding } from "react-native-polyfill-globals/src/encoding";
import { polyfill as polyfillReadableStream } from "react-native-polyfill-globals/src/readable-stream";
import { polyfill as polyfillFetch } from "react-native-polyfill-globals/src/fetch";

polyfillReadableStream();
polyfillEncoding();
polyfillFetch();

HttpLinkにストリーミングオプションを追加します。

const httpLink = new HttpLink({
  uri: "https://your-api.example.com/graphql",
  fetchOptions: {
    reactNative: { textStreaming: true },
  },
});

urqlによる軽量セットアップ

urqlはバンドルサイズがApollo Clientの半分以下で、Exchange(プラグイン)によるモジュラーな設計が特長です。

パッケージのインストール

npx expo install urql graphql

基本セットアップ

import { Client, Provider, cacheExchange, fetchExchange } from "urql";

const urqlClient = new Client({
  url: "https://your-api.example.com/graphql",
  exchanges: [cacheExchange, fetchExchange],
});

export default function App() {
  return (
    <Provider value={urqlClient}>
      <Navigation />
    </Provider>
  );
}

Queryの実装

import { useQuery, gql } from "urql";
import { View, Text, FlatList, ActivityIndicator } from "react-native";

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

function UserList() {
  const [result] = useQuery({ query: GET_USERS });
  const { data, fetching, error } = result;

  if (fetching) return <ActivityIndicator size="large" />;
  if (error) return <Text>エラー: {error.message}</Text>;

  return (
    <FlatList
      data={data?.users}
      keyExtractor={(item: { id: string }) => item.id}
      renderItem={({ item }) => (
        <View style={{ padding: 16 }}>
          <Text style={{ fontWeight: "bold" }}>{item.name}</Text>
          <Text>{item.email}</Text>
        </View>
      )}
    />
  );
}

正規化キャッシュの追加

urqlの標準キャッシュはドキュメントキャッシュ(クエリ単位)です。エンティティ単位の正規化キャッシュが必要な場合は@urql/exchange-graphcacheを追加します。

npx expo install @urql/exchange-graphcache
import { Client, fetchExchange } from "urql";
import { cacheExchange } from "@urql/exchange-graphcache";

const urqlClient = new Client({
  url: "https://your-api.example.com/graphql",
  exchanges: [
    cacheExchange({
      keys: {
        // エンティティのキー設定
      },
    }),
    fetchExchange,
  ],
});

GraphQL Code Generatorで型安全性を確保

GraphQL Code Generator(@graphql-codegen/cli v6.x)を使うと、GraphQLスキーマからTypeScriptの型定義を自動生成できます。手動でinterfaceを定義する必要がなくなり、スキーマ変更時の型不整合を防止できます。

インストール

npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/client-preset

codegen.tsの設定

プロジェクトルートにcodegen.tsを作成します。

import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  schema: "https://your-api.example.com/graphql",
  documents: ["src/**/*.{ts,tsx}"],
  generates: {
    "./src/__generated__/": {
      preset: "client",
      config: {
        documentMode: "string",
      },
    },
  },
};

export default config;

package.jsonにスクリプトを追加

{
  "scripts": {
    "codegen": "graphql-codegen --config codegen.ts",
    "codegen:watch": "graphql-codegen --config codegen.ts --watch"
  }
}

生成された型の利用

型生成を実行すると、src/__generated__/ディレクトリにTypeScriptファイルが出力されます。

npm run codegen

生成された型付きドキュメントをコンポーネントで利用します。

import { useQuery } from "@apollo/client/react";
import { graphql } from "../__generated__/gql";

const GET_USERS = graphql(`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`);

function UserList() {
  // data の型が自動的に推論される
  const { loading, data } = useQuery(GET_USERS);

  return (
    <FlatList
      data={data?.users}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <Text>{item.name}</Text>}
    />
  );
}

Fragment Colocationパターン

コンポーネントが必要とするデータをFragmentとして宣言し、親コンポーネントのクエリに組み込むパターンです。データ依存関係が明確になり、大規模アプリの保守性が向上します。

// UserCard.tsx
import { graphql, FragmentType, useFragment } from "../__generated__/gql";

const UserCardFragment = graphql(`
  fragment UserCardFields on User {
    id
    name
    avatarUrl
  }
`);

interface UserCardProps {
  user: FragmentType<typeof UserCardFragment>;
}

function UserCard({ user }: UserCardProps) {
  const data = useFragment(UserCardFragment, user);

  return (
    <View style={{ flexDirection: "row", alignItems: "center" }}>
      <Image source={{ uri: data.avatarUrl }} style={{ width: 40, height: 40 }} />
      <Text>{data.name}</Text>
    </View>
  );
}

// UserListScreen.tsx
const GET_USERS = graphql(`
  query GetUsers {
    users {
      ...UserCardFields
    }
  }
`);

function UserListScreen() {
  const { data } = useQuery(GET_USERS);

  return (
    <FlatList
      data={data?.users}
      renderItem={({ item }) => <UserCard user={item} />}
    />
  );
}

Subscriptionによるリアルタイム通信

GraphQL Subscriptionは、サーバーからクライアントへリアルタイムにデータをプッシュする仕組みです。チャット・通知・ライブフィードなどの機能に適しています。

Apollo ClientでのWebSocket Subscription

npx expo install graphql-ws
import { ApolloClient, InMemoryCache, HttpLink, split } from "@apollo/client";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { getMainDefinition } from "@apollo/client/utilities";
import { createClient } from "graphql-ws";

const httpLink = new HttpLink({
  uri: "https://your-api.example.com/graphql",
});

const wsLink = new GraphQLWsLink(
  createClient({
    url: "wss://your-api.example.com/graphql",
  })
);

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === "OperationDefinition" &&
      definition.operation === "subscription"
    );
  },
  wsLink,
  httpLink
);

export const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

コンポーネントでの利用:

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

const MESSAGE_ADDED = gql`
  subscription OnMessageAdded($channelId: ID!) {
    messageAdded(channelId: $channelId) {
      id
      text
      createdAt
      sender {
        name
      }
    }
  }
`;

function ChatMessages({ channelId }: { channelId: string }) {
  const { data, loading } = useSubscription(MESSAGE_ADDED, {
    variables: { channelId },
  });

  // data?.messageAdded に新着メッセージが入る
}

キャッシュ戦略とパフォーマンス最適化

Apollo Clientのキャッシュ設計

Apollo ClientのInMemoryCacheは、レスポンスデータを__typenameidフィールドの組み合わせで正規化して保存します。同一エンティティが複数のクエリで返されても、キャッシュ内では1つの参照として統合されます。

const cache = new InMemoryCache({
  typePolicies: {
    User: {
      // カスタムキーフィールド
      keyFields: ["userId"],
    },
    Query: {
      fields: {
        users: {
          // ページネーションのマージ戦略
          keyArgs: false,
          merge(existing = [], incoming) {
            return [...existing, ...incoming];
          },
        },
      },
    },
  },
});

FlatListとの統合でパフォーマンスを確保

React NativeのFlatListは大量データの表示に最適化されていますが、GraphQLクエリとの組み合わせでは注意点があります。

function UserListOptimized() {
  const { data, loading, fetchMore } = useQuery(GET_USERS, {
    variables: { offset: 0, limit: 20 },
  });

  const loadMore = () => {
    fetchMore({
      variables: {
        offset: data?.users.length ?? 0,
      },
    });
  };

  return (
    <FlatList
      data={data?.users}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <UserCard user={item} />}
      onEndReached={loadMore}
      onEndReachedThreshold={0.5}
      // 不要な再レンダリングを防止
      getItemLayout={(_, index) => ({
        length: 80,
        offset: 80 * index,
        index,
      })}
    />
  );
}

ネットワーク状態に応じたフェッチポリシー

モバイルアプリではネットワーク状態が不安定なことが多いため、フェッチポリシーの使い分けが重要です。

フェッチポリシー動作適した場面
cache-firstキャッシュ優先、なければネットワーク頻繁に変わらないデータ(プロフィールなど)
network-only常にネットワークから取得リアルタイム性が求められるデータ
cache-and-networkキャッシュを即返し、裏でネットワーク更新一覧画面の初期表示
no-cacheキャッシュを使わないワンタイムのデータ取得
const { data } = useQuery(GET_TIMELINE, {
  fetchPolicy: "cache-and-network",
});

New Architecture(Fabric / TurboModules)との互換性

React Native 0.76(2024年)でNew Architectureがデフォルトとなり、0.82(2025年10月リリース)では旧アーキテクチャが完全に廃止され、New Architecture(Fabric + TurboModules + JSI)が唯一のアーキテクチャとして必須化されました。Apollo Client、urqlともにJavaScriptレイヤーで動作するため、New Architectureとの互換性に問題はありません。

JSI(JavaScript Interface)によりJavaScriptとネイティブ層が同期的に通信できるようになりましたが、GraphQLクライアントはHTTPレイヤーで動作するため、この変更が直接的に影響を与えることはありません。ただし、ネイティブモジュール経由でのネットワーク通信が高速化される恩恵は間接的に受けられます。

よくあるトラブルと対処法

キャッシュの不整合

Apollo Clientのキャッシュは自動更新が基本ですが、Mutationの戻り値に含まれないフィールドが更新されないケースがあります。

対処法: Mutationのupdate関数でキャッシュを手動更新するか、refetchQueriesで関連クエリを再取得します。

const [deleteUser] = useMutation(DELETE_USER, {
  update(cache, { data }) {
    cache.modify({
      fields: {
        users(existingUsers = [], { readField }) {
          return existingUsers.filter(
            (ref: any) => readField("id", ref) !== data?.deleteUser.id
          );
        },
      },
    });
  },
});

React NativeでのTextEncoder未定義エラー

一部のGraphQLクライアントがTextEncoderを使用しますが、React Native環境にはデフォルトで存在しません。

対処法: react-native-polyfill-globalsを導入してポリフィルを適用します(前述のテキストストリーミング対応セクションを参照)。

Expoでのmetro.config.js設定

Expoプロジェクトで.graphqlファイルを直接インポートする場合、metroの設定変更が必要です。

// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");

const config = getDefaultConfig(__dirname);
config.resolver.sourceExts.push("graphql", "gql");

module.exports = config;

ただし、GraphQL Code Generatorを使う場合は.graphqlファイルのインポートは不要で、TypeScriptファイル内にgqlタグで直接記述する方式が推奨されます。

プロジェクト構成の実践例

中〜大規模のReact Native + GraphQLプロジェクトでは、以下のディレクトリ構成が保守性と拡張性のバランスに優れています。

src/
├── __generated__/       # GraphQL Code Generator出力
│   ├── gql.ts
│   ├── graphql.ts
│   └── fragment-masking.ts
├── graphql/
│   ├── queries/          # Query定義
│   │   ├── user.ts
│   │   └── post.ts
│   ├── mutations/        # Mutation定義
│   │   └── user.ts
│   ├── subscriptions/    # Subscription定義
│   │   └── message.ts
│   └── fragments/        # 共有Fragment
│       └── userFields.ts
├── lib/
│   └── apollo.ts         # クライアント初期化
├── screens/
│   ├── UserListScreen.tsx
│   └── ChatScreen.tsx
└── components/
    ├── UserCard.tsx
    └── MessageBubble.tsx

まとめ

React NativeとGraphQLの組み合わせは、モバイルアプリのデータ取得を効率化する強力な手段です。Apollo Client 4.0は正規化キャッシュやSubscriptionを含む包括的な機能を提供し、urqlはバンドルサイズを抑えつつ十分な拡張性を備えています。graphql-request + TanStack Queryの組み合わせは、シンプルさを重視するプロジェクトに適しています。

GraphQL Code Generatorによる型自動生成を加えれば、スキーマとフロントエンドの型が常に同期し、実行時エラーを大幅に削減できます。プロジェクトの規模・チームの経験・パフォーマンス要件を踏まえて、最適なクライアントを選定してください。