RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-03-24上級

Rork × GraphQL実装ガイド — Apollo Clientで構築するリアルタイムデータ連携アプリ

RorkアプリにApollo ClientでGraphQLを統合する完全ガイド。スキーマ設計からクエリ・ミューテーション・サブスクリプション、キャッシュ最適化まで実践的なコード例とともに解説します。

GraphQLApollo ClientRork515React Native209リアルタイム7API連携3上級7

なぜ GraphQL × Rork なのか

REST APIが長らくモバイル開発の標準でしたが、GraphQL はその課題を根本から解決する設計思想を持っています。「必要なフィールドだけ取得できる」「1回のリクエストで複数リソースを結合できる」「リアルタイムサブスクリプションが標準仕様」という三つの強みは、複雑なUIを持つRorkアプリと非常に相性が良いのです。

この実装で扱う範囲:

  • Apollo Clientのセットアップとプロバイダー設定
  • 型安全なクエリ・ミューテーションの実装パターン
  • WebSocketを使ったリアルタイムサブスクリプション
  • InMemoryCacheによるキャッシュ最適化テクニック
  • エラーハンドリングとローディング状態の管理

対象読者: React NativeおよびRorkの基礎知識があり、より洗練されたAPIアーキテクチャを導入したい中〜上級の開発者


前提知識と環境準備

必要な環境

  • Rork Max(推奨)またはRork Pro
  • Node.js 18以上
  • GraphQLバックエンド(ここではHasuraを例として使用)

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

RorkプロジェクトのAIチャットで以下を指示し、必要なパッケージを追加します:

# Apollo Client関連パッケージ
npm install @apollo/client graphql
 
# WebSocketサポート(サブスクリプション用)
npm install graphql-ws
 
# 型生成ツール(推奨)
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

GraphQLクライアントのセットアップ

Apollo Clientの初期化

まず、Apollo Clientのインスタンスを作成するファイルを用意します:

// 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接続(クエリ・ミューテーション用)
const httpLink = createHttpLink({
  uri: 'https://your-hasura-instance.hasura.app/v1/graphql',
  headers: {
    'x-hasura-admin-secret': process.env.EXPO_PUBLIC_HASURA_SECRET || '',
  },
});
 
// WebSocket接続(サブスクリプション用)
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 || '',
      },
    },
  })
);
 
// 操作の種類に応じてリンクを自動選択
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,  // サブスクリプション → WebSocket
  httpLink // クエリ・ミューテーション → HTTP
);
 
// キャッシュ設定
const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        todos: {
          // ページネーションのマージ設定
          keyArgs: ['where'],
          merge(existing = [], incoming) {
            return [...existing, ...incoming];
          },
        },
      },
    },
  },
});
 
export const client = new ApolloClient({
  link: splitLink,
  cache,
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network', // キャッシュ優先、バックグラウンドで更新
    },
  },
});

プロバイダーの設定

app/_layout.tsx(App Router使用の場合)で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>
  );
}

クエリの実装 — データ取得パターン

基本的なクエリ

useQueryフックを使ったシンプルなデータ取得の例です:

// hooks/useTodos.ts
import { gql, useQuery } from '@apollo/client';
 
// GraphQLクエリの定義(型安全)
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, // userIdがない場合はスキップ
    }
  );
 
  return {
    todos: data?.todos ?? [],
    loading,
    error,
    refetch,
  };
}

コンポーネントでの使用例

// 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}>データを取得中...</Text>
      </View>
    );
  }
 
  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>
          エラーが発生しました: {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>
      )}
    />
  );
}
 
// スタイルは省略

ミューテーションの実装 — データ更新パターン

楽観的更新(Optimistic UI)

ユーザー体験を向上させるために、サーバーのレスポンスを待たずにUIを先に更新する「楽観的更新」を実装します:

// 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, {
    // 楽観的更新: サーバーレスポンスを待たずにキャッシュを更新
    optimisticResponse: ({ title, priority }) => ({
      insert_todos_one: {
        __typename: 'todos',
        id: `temp-${Date.now()}`, // 一時的なID
        title,
        completed: false,
        created_at: new Date().toISOString(),
        priority,
      },
    }),
    update(cache, { data }) {
      // キャッシュに新しいTODOを追加
      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, {
    // 削除後にキャッシュからも除去
    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,
  };
}

サブスクリプション — リアルタイム更新の実装

GraphQLの真価はリアルタイムサブスクリプションにあります。チャットアプリや共同編集ツールを作る際に特に威力を発揮します:

// 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('リアルタイム更新受信:', subscriptionData.data?.todos?.length, '件');
      },
      onSubscriptionComplete: () => {
        console.log('サブスクリプション終了');
      },
    }
  );
 
  return {
    todos: data?.todos ?? [],
    loading,
    error,
  };
}

実行結果の例: サーバー側でTODOが追加・更新・削除されると、接続中のすべてのクライアントに即座に反映されます。WebSocket接続が維持されている限り、polling や手動 refetch 不要でUIが自動更新されます。


キャッシュ最適化 — 高パフォーマンスのための設計

Apollo Clientの InMemoryCache を適切に設定することで、ネットワークリクエストを削減し、UXを向上させることができます:

// lib/apolloClient.ts(キャッシュ設定の詳細版)
const cache = new InMemoryCache({
  typePolicies: {
    // Todoエンティティのキャッシュポリシー
    todos: {
      keyFields: ['id'], // ユニークキー(デフォルトはid)
    },
    Query: {
      fields: {
        // オフセットベースのページネーション
        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);
          },
        },
      },
    },
  },
});

fetchPolicy の使い分け

fetchPolicyユースケース説明
cache-first(デフォルト)静的データキャッシュにあればネットワーク不要
cache-and-networkダッシュボードキャッシュ表示後バックグラウンド更新
network-only決済・認証常に最新データを取得
no-cacheセンシティブデータキャッシュしない
cache-onlyオフラインモードネットワークを使わない

よくあるエラーと対処法

「Network error: Failed to fetch」が発生する → エンドポイントURLとヘッダー(認証トークン)を確認してください。ExpoのAndroidエミュレーターからlocalhostに接続する場合は http://10.0.2.2:4000/graphql を使用します。

「Cache data may be lost when replacing the todos field」警告が出るtypePolicies で該当フィールドの merge 関数を定義することで解決します。上記のキャッシュ設定を参照してください。

サブスクリプションが接続されない → WebSocketのURLが wss://(TLS)になっているか確認してください。開発環境では ws:// でも動作しますが、本番環境では必ずTLSを使用してください。

「Cannot query field X on type Y」エラー → GraphQLスキーマと実際のクエリのフィールド名が不一致です。HasuraのGraphiQL UIでスキーマを確認してクエリを修正してください。


応用 — 認証との統合

JWTトークンを使った認証付きリクエストの実装例:

// lib/apolloClient.ts(認証統合版)
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}` : '',
    },
  };
});
 
// リンクの連結: authLink → httpLink
const authenticatedHttpLink = authLink.concat(httpLink);

詳細な認証実装については、Rork × Firebase/Supabase認証ガイドを参照してください。

また、GraphQL × Supabaseの組み合わせでリアルタイムチャットを実装する具体例は、SupabaseチャットアプリをRorkで構築するで詳しく解説しています。

状態管理との統合パターンについては、Rorkアプリの状態管理パターン完全ガイドもあわせてご覧ください。


まとめ

ここではRorkアプリにApollo Clientを統合してGraphQL APIと連携するための実践的な手法を解説しました。

GraphQLの導入は初期コストがかかりますが、一度設定してしまえば、必要なデータだけを効率よく取得でき、リアルタイム機能の実装も大幅に簡単になります。特に複数の画面でデータを共有したり、リアルタイムコラボレーション機能を持つアプリには、今回紹介したApollo Clientのキャッシュ戦略と楽観的更新が非常に有効です。

ぜひ本記事のコードを参考に、Rorkアプリのデータ連携を次のレベルへと引き上げてみてください。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-04-07
Rork Max × Liveblocks / Yjs:リアルタイム協調編集アプリ開発
Rork MaxアプリにLiveblocksとYjsを統合し、複数ユーザーが同時編集できるリアルタイム協調機能を実装する完全ガイド。CRDTの仕組みから本番運用まで解説します。
開発ツール2026-04-04
Rork × Firebase Realtime Database でリアルタイム同期アプリを作る入門ガイド
Rork アプリに Firebase Realtime Database を連携する方法をゼロから解説。データのリアルタイム同期・CRUD操作・オフライン対応まで、実践コード付きで丁寧に説明します。
開発ツール2026-07-15
「、」が行頭に落ちた — Rork が組んだ名言カードと日本語の禁則処理
React Native の Text は日本語の行頭禁則を保証しません。実測した違反率、WORD JOINER による事前結合の実装、onTextLayout での自動監査、Rork Max(SwiftUI)との挙動差までまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →