T-CREATOR

useQuery から useLazyQuery まで - Apollo Hooks 活用パターン集

useQuery から useLazyQuery まで - Apollo Hooks 活用パターン集

現代の React アプリケーション開発では、GraphQL を活用したデータ取得が主流となっています。その中でも、Apollo Client が提供する Hooks は、開発者にとって強力なツールです。

今回の記事では、useQuery から useLazyQuery まで、Apollo Client Hooks の基本的な使い方から応用的なパターンまでを網羅的に解説いたします。実際の開発現場で役立つ実践的なコード例とともに、効率的なデータ管理の手法をお伝えしますね。

背景

GraphQL API と React アプリケーションの連携において、従来の RESTful API とは異なる課題が存在しました。特にデータの取得タイミングやキャッシュ管理、リアルタイム更新などの複雑な要件を満たすために、多くの開発者が独自の解決策を模索していたのです。

mermaidflowchart TD
    A[従来のRESTful API] --> B[複数エンドポイント]
    A --> C[Over-fetching問題]
    A --> D[Under-fetching問題]

    E[GraphQL + Apollo Client] --> F[単一エンドポイント]
    E --> G[必要なデータのみ取得]
    E --> H[統一されたキャッシュ管理]
    E --> I[型安全なクエリ]

Apollo Client の登場により、これらの課題が解決され、より効率的な開発が可能になりました。

課題

React アプリケーションでデータを取得する際に、開発者が直面する主な課題をご説明します。

データ取得タイミングの制御

コンポーネントのマウント時に自動的にデータを取得する場合と、ユーザーの操作に応じて取得する場合では、異なるアプローチが必要でした。

mermaidstateDiagram-v2
    [*] --> ComponentMount
    ComponentMount --> AutoFetch : useQuery
    ComponentMount --> WaitForAction : useLazyQuery

    AutoFetch --> DataLoaded
    WaitForAction --> UserAction
    UserAction --> ManualFetch
    ManualFetch --> DataLoaded

    DataLoaded --> [*]

状態管理の複雑さ

ローディング状態、エラーハンドリング、データの更新など、多くの状態を適切に管理する必要がありました。従来の方法では、これらを個別に実装する必要があり、コードの複雑化が問題となっていたのです。

リアルタイム通信の実装

チャット機能やライブダッシュボードなど、リアルタイムでデータを更新する機能の実装には、WebSocket や SSE などの技術的な知識が必要でした。

解決策

Apollo Client Hooks は、これらの課題を統一的な API で解決します。各 Hook は特定の目的に最適化されており、開発者は適切な Hook を選択することで、効率的にデータ管理を実現できるのです。

mermaidflowchart LR
    A[Apollo Client Hooks] --> B[useQuery]
    A --> C[useLazyQuery]
    A --> D[useMutation]
    A --> E[useSubscription]

    B --> F[自動データ取得]
    C --> G[手動データ取得]
    D --> H[データ更新]
    E --> I[リアルタイム通信]

この解決策により、統一された方法でデータ管理が可能になり、開発効率が大幅に向上します。

具体例

基本的な Hook の理解

useQuery の基礎

useQuery は、コンポーネントのマウント時に自動的に GraphQL クエリを実行する Hook です。最も基本的なデータ取得パターンを実現します。

まず、基本的なクエリの定義から見てみましょう。

javascriptimport { gql } from '@apollo/client';

// GraphQLクエリを定義
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      createdAt
    }
  }
`;

続いて、useQuery を使用してデータを取得するコンポーネントを実装します。

javascriptimport React from 'react';
import { useQuery } from '@apollo/client';

const UserList = () => {
  // useQueryでデータを取得
  const { loading, error, data } = useQuery(GET_USERS);

  // ローディング状態の表示
  if (loading) return <div>読み込み中...</div>;

  // エラー状態の表示
  if (error)
    return <div>エラーが発生しました: {error.message}</div>;

  return (
    <div>
      <h2>ユーザー一覧</h2>
      {data.users.map((user) => (
        <div key={user.id}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  );
};

オプション設定による詳細制御

useQuery では、豊富なオプションを設定できます。これにより、キャッシュポリシーやポーリング間隔などを柔軟に制御できるのです。

javascriptconst UserListWithOptions = () => {
  const { loading, error, data, refetch } = useQuery(
    GET_USERS,
    {
      // キャッシュポリシーを設定
      fetchPolicy: 'cache-and-network',
      // 5秒間隔でポーリング
      pollInterval: 5000,
      // エラー時の再試行回数
      errorPolicy: 'all',
      // 変数を指定
      variables: {
        limit: 10,
        offset: 0,
      },
    }
  );

  return (
    <div>
      {loading && <div>更新中...</div>}
      {error && (
        <div>
          <p>エラーが発生しました: {error.message}</p>
          <button onClick={() => refetch()}>
            再読み込み
          </button>
        </div>
      )}
      {data && (
        <div>
          {data.users.map((user) => (
            <div key={user.id}>{user.name}</div>
          ))}
          <button onClick={() => refetch()}>
            手動更新
          </button>
        </div>
      )}
    </div>
  );
};

ローディング・エラー・データの状態管理

Apollo Client は、データ取得の各段階で適切な状態を提供します。この状態管理を活用して、ユーザーフレンドリーな UI を構築できますね。

javascriptconst UserListWithDetailedStates = () => {
  const { loading, error, data, networkStatus } = useQuery(
    GET_USERS,
    {
      notifyOnNetworkStatusChange: true,
    }
  );

  // 詳細な状態判定
  const isInitialLoading = loading && !data;
  const isRefetching = networkStatus === 4;
  const hasData = data && data.users;

  return (
    <div>
      {isInitialLoading && (
        <div className='loading-spinner'>
          <span>初回読み込み中...</span>
        </div>
      )}

      {isRefetching && (
        <div className='refetch-indicator'>
          <span>データを更新中...</span>
        </div>
      )}

      {error && (
        <div className='error-message'>
          <h3>エラーが発生しました</h3>
          <p>{error.message}</p>
          <details>
            <summary>詳細情報</summary>
            <pre>{JSON.stringify(error, null, 2)}</pre>
          </details>
        </div>
      )}

      {hasData && (
        <div className='user-list'>
          <h2>ユーザー一覧 ({data.users.length}人)</h2>
          {data.users.map((user) => (
            <div key={user.id} className='user-card'>
              <h3>{user.name}</h3>
              <p>{user.email}</p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

useMutation の基礎

useMutation は、データの作成、更新、削除を行うための Hook です。GraphQL の mutation を実行し、サーバー上のデータを変更する際に使用します。

基本的な使い方

まず、mutation 用の GraphQL クエリを定義します。

javascriptconst CREATE_USER = gql`
  mutation CreateUser($input: UserInput!) {
    createUser(input: $input) {
      id
      name
      email
      createdAt
    }
  }
`;

次に、useMutation を使用してユーザー作成機能を実装します。

javascriptimport React, { useState } from 'react';
import { useMutation } from '@apollo/client';

const CreateUserForm = () => {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
  });

  // useMutationでmutation関数と状態を取得
  const [createUser, { loading, error, data }] =
    useMutation(CREATE_USER);

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const result = await createUser({
        variables: {
          input: formData,
        },
      });
      console.log(
        'ユーザーが作成されました:',
        result.data.createUser
      );
      // フォームをリセット
      setFormData({ name: '', email: '' });
    } catch (err) {
      console.error('ユーザー作成エラー:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>名前:</label>
        <input
          type='text'
          value={formData.name}
          onChange={(e) =>
            setFormData({
              ...formData,
              name: e.target.value,
            })
          }
          disabled={loading}
        />
      </div>
      <div>
        <label>メール:</label>
        <input
          type='email'
          value={formData.email}
          onChange={(e) =>
            setFormData({
              ...formData,
              email: e.target.value,
            })
          }
          disabled={loading}
        />
      </div>

      <button type='submit' disabled={loading}>
        {loading ? '作成中...' : 'ユーザー作成'}
      </button>

      {error && <p>エラー: {error.message}</p>}
      {data && <p>ユーザーが正常に作成されました!</p>}
    </form>
  );
};

楽観的更新

楽観的更新は、サーバーからのレスポンスを待たずに UI を即座に更新する手法です。これにより、ユーザーエクスペリエンスが大幅に向上します。

javascriptconst OptimisticUserUpdate = () => {
  const [updateUser] = useMutation(UPDATE_USER);

  const handleUpdateUser = (userId, newData) => {
    updateUser({
      variables: { id: userId, input: newData },
      // 楽観的更新の設定
      optimisticResponse: {
        updateUser: {
          __typename: 'User',
          id: userId,
          ...newData,
          // 更新日時を現在時刻で設定
          updatedAt: new Date().toISOString(),
        },
      },
      // キャッシュ更新の処理
      update: (cache, { data: { updateUser } }) => {
        // 既存のクエリキャッシュを更新
        const existingUsers = cache.readQuery({
          query: GET_USERS,
        });
        const updatedUsers = existingUsers.users.map(
          (user) =>
            user.id === updateUser.id ? updateUser : user
        );

        cache.writeQuery({
          query: GET_USERS,
          data: { users: updatedUsers },
        });
      },
    });
  };

  return (
    <button
      onClick={() =>
        handleUpdateUser('1', { name: '更新された名前' })
      }
    >
      ユーザー情報を更新
    </button>
  );
};

エラーハンドリング

適切なエラーハンドリングは、堅牢なアプリケーションには欠かせません。

javascriptconst UserFormWithErrorHandling = () => {
  const [createUser, { loading, error }] = useMutation(
    CREATE_USER,
    {
      // エラーポリシーを設定
      errorPolicy: 'all',
      // エラー発生時のコールバック
      onError: (error) => {
        console.error('Mutation error:', error);
        // エラーロギングサービスに送信
        // logError(error);
      },
      // 成功時のコールバック
      onCompleted: (data) => {
        console.log(
          'User created successfully:',
          data.createUser
        );
        // 成功メッセージの表示や画面遷移
      },
    }
  );

  const getErrorMessage = (error) => {
    if (error.networkError) {
      return 'ネットワークエラーが発生しました。しばらくしてから再試行してください。';
    }

    if (error.graphQLErrors.length > 0) {
      return error.graphQLErrors
        .map((err) => err.message)
        .join(', ');
    }

    return '予期しないエラーが発生しました。';
  };

  return (
    <div>
      {error && (
        <div className='error-alert'>
          <h4>エラーが発生しました</h4>
          <p>{getErrorMessage(error)}</p>
          <details>
            <summary>技術的詳細</summary>
            <pre>{JSON.stringify(error, null, 2)}</pre>
          </details>
        </div>
      )}

      <form>
        {/* フォーム要素 */}
        <button type='submit' disabled={loading}>
          {loading ? '処理中...' : '送信'}
        </button>
      </form>
    </div>
  );
};

条件付きデータ取得パターン

useLazyQuery の活用

useLazyQuery は、useQuery とは異なり、コンポーネントのマウント時に自動実行されません。ユーザーのアクションやアプリケーションの特定の条件が満たされた時にのみクエリを実行します。

オンデマンドクエリ実行

検索機能やタブ切り替えなど、必要な時にだけデータを取得したい場合に最適です。

javascriptimport { useLazyQuery } from '@apollo/client';

const SearchUsers = () => {
  const [searchTerm, setSearchTerm] = useState('');

  // useLazyQueryでクエリ実行関数を取得
  const [searchUsers, { loading, error, data, called }] =
    useLazyQuery(SEARCH_USERS, {
      // 検索結果をキャッシュしない設定
      fetchPolicy: 'no-cache',
    });

  const handleSearch = () => {
    if (searchTerm.trim()) {
      searchUsers({
        variables: { searchTerm: searchTerm.trim() },
      });
    }
  };

  return (
    <div>
      <div className='search-form'>
        <input
          type='text'
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder='ユーザー名で検索...'
          onKeyPress={(e) =>
            e.key === 'Enter' && handleSearch()
          }
        />
        <button onClick={handleSearch} disabled={loading}>
          {loading ? '検索中...' : '検索'}
        </button>
      </div>

      {called && (
        <div className='search-results'>
          {loading && <div>検索中...</div>}

          {error && (
            <div className='error'>
              検索エラー: {error.message}
            </div>
          )}

          {data && (
            <div>
              <h3>
                検索結果 ({data.searchUsers.length}件)
              </h3>
              {data.searchUsers.length === 0 ? (
                <p>
                  該当するユーザーが見つかりませんでした。
                </p>
              ) : (
                data.searchUsers.map((user) => (
                  <div
                    key={user.id}
                    className='search-result-item'
                  >
                    <h4>{user.name}</h4>
                    <p>{user.email}</p>
                  </div>
                ))
              )}
            </div>
          )}
        </div>
      )}
    </div>
  );
};

ユーザーアクション起点のデータ取得

ボタンクリックやモーダルオープンなど、特定のユーザーアクションに応じてデータを取得する実装例です。

javascriptconst UserDetailsModal = ({ userId, isOpen, onClose }) => {
  // ユーザー詳細情報取得用のlazy query
  const [getUserDetails, { loading, error, data }] =
    useLazyQuery(GET_USER_DETAILS);

  // モーダルが開かれた時にデータを取得
  useEffect(() => {
    if (isOpen && userId) {
      getUserDetails({
        variables: { userId },
      });
    }
  }, [isOpen, userId, getUserDetails]);

  if (!isOpen) return null;

  return (
    <div className='modal-overlay'>
      <div className='modal-content'>
        <div className='modal-header'>
          <h2>ユーザー詳細</h2>
          <button onClick={onClose}>×</button>
        </div>

        <div className='modal-body'>
          {loading && <div>詳細情報を読み込み中...</div>}

          {error && (
            <div className='error-message'>
              詳細情報の取得に失敗しました: {error.message}
            </div>
          )}

          {data && data.userDetails && (
            <div className='user-details'>
              <img
                src={data.userDetails.avatar}
                alt={data.userDetails.name}
              />
              <h3>{data.userDetails.name}</h3>
              <p>メール: {data.userDetails.email}</p>
              <p>
                登録日:{' '}
                {new Date(
                  data.userDetails.createdAt
                ).toLocaleDateString()}
              </p>
              <p>
                最終ログイン:{' '}
                {new Date(
                  data.userDetails.lastLogin
                ).toLocaleString()}
              </p>

              <div className='user-stats'>
                <h4>統計情報</h4>
                <p>投稿数: {data.userDetails.postCount}</p>
                <p>
                  フォロワー数:{' '}
                  {data.userDetails.followerCount}
                </p>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

検索機能での活用例

リアルタイム検索やデバウンス機能を組み合わせた実践的な検索実装です。

javascriptimport { useState, useEffect, useCallback } from 'react';
import { useLazyQuery } from '@apollo/client';

const RealTimeSearch = () => {
  const [query, setQuery] = useState('');
  const [debouncedQuery, setDebouncedQuery] = useState('');

  // 検索用のlazy query
  const [searchItems, { loading, error, data }] =
    useLazyQuery(SEARCH_ITEMS, {
      fetchPolicy: 'cache-and-network',
    });

  // デバウンス処理
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedQuery(query);
    }, 300); // 300ms後に実行

    return () => clearTimeout(timer);
  }, [query]);

  // デバウンスされたクエリで検索実行
  useEffect(() => {
    if (debouncedQuery.length >= 2) {
      searchItems({
        variables: { searchQuery: debouncedQuery },
      });
    }
  }, [debouncedQuery, searchItems]);

  // 検索結果のハイライト表示
  const highlightText = useCallback((text, highlight) => {
    if (!highlight) return text;

    const parts = text.split(
      new RegExp(`(${highlight})`, 'gi')
    );
    return parts.map((part, index) =>
      part.toLowerCase() === highlight.toLowerCase() ? (
        <mark key={index}>{part}</mark>
      ) : (
        part
      )
    );
  }, []);

  return (
    <div className='real-time-search'>
      <div className='search-input-container'>
        <input
          type='text'
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder='商品名で検索...'
          className='search-input'
        />
        {loading && (
          <div className='search-spinner'>🔍</div>
        )}
      </div>

      {query.length > 0 && query.length < 2 && (
        <div className='search-hint'>
          2文字以上入力してください
        </div>
      )}

      {error && (
        <div className='search-error'>
          検索エラー: {error.message}
        </div>
      )}

      {data && data.searchItems && (
        <div className='search-results'>
          <div className='results-header'>
            {data.searchItems.length}
            件の結果が見つかりました
          </div>

          {data.searchItems.map((item) => (
            <div
              key={item.id}
              className='search-result-item'
            >
              <img src={item.thumbnail} alt={item.name} />
              <div className='item-details'>
                <h4>
                  {highlightText(item.name, debouncedQuery)}
                </h4>
                <p className='item-description'>
                  {highlightText(
                    item.description,
                    debouncedQuery
                  )}
                </p>
                <div className='item-meta'>
                  <span className='price'>
                    ¥{item.price.toLocaleString()}
                  </span>
                  <span className='category'>
                    {item.category}
                  </span>
                </div>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

高度な活用パターン

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

useSubscription は、WebSocket を使用してサーバーからリアルタイムでデータを受信するための Hook です。チャット機能、ライブ更新、通知システムなどで威力を発揮します。

WebSocket 接続

まず、subscription 用の GraphQL クエリを定義します。

javascriptconst MESSAGE_SUBSCRIPTION = gql`
  subscription OnMessageAdded($chatId: ID!) {
    messageAdded(chatId: $chatId) {
      id
      content
      user {
        id
        name
        avatar
      }
      createdAt
    }
  }
`;

ライブデータ更新

リアルタイムチャット機能の実装例をご紹介します。

javascriptimport { useSubscription, useQuery } from '@apollo/client';

const ChatRoom = ({ chatId }) => {
  // 既存メッセージの取得
  const { data: messagesData, loading } = useQuery(
    GET_MESSAGES,
    {
      variables: { chatId },
    }
  );

  // 新しいメッセージのリアルタイム受信
  const { data: subscriptionData } = useSubscription(
    MESSAGE_SUBSCRIPTION,
    {
      variables: { chatId },
      onSubscriptionData: ({
        client,
        subscriptionData,
      }) => {
        // 新しいメッセージを既存のキャッシュに追加
        const newMessage =
          subscriptionData.data.messageAdded;

        const existingMessages = client.readQuery({
          query: GET_MESSAGES,
          variables: { chatId },
        });

        client.writeQuery({
          query: GET_MESSAGES,
          variables: { chatId },
          data: {
            messages: [
              ...existingMessages.messages,
              newMessage,
            ],
          },
        });
      },
    }
  );

  if (loading)
    return <div>チャット履歴を読み込み中...</div>;

  return (
    <div className='chat-room'>
      <div className='chat-header'>
        <h2>チャットルーム</h2>
        <div className='connection-status'>
          {subscriptionData ? '🟢 接続中' : '🔴 切断中'}
        </div>
      </div>

      <div className='messages-container'>
        {messagesData?.messages.map((message) => (
          <div key={message.id} className='message'>
            <div className='message-header'>
              <img
                src={message.user.avatar}
                alt={message.user.name}
              />
              <span className='username'>
                {message.user.name}
              </span>
              <span className='timestamp'>
                {new Date(
                  message.createdAt
                ).toLocaleTimeString()}
              </span>
            </div>
            <div className='message-content'>
              {message.content}
            </div>
          </div>
        ))}
      </div>

      <MessageInput chatId={chatId} />
    </div>
  );
};

複数 Hook の連携パターン

複数の Apollo Hooks を組み合わせることで、より複雑な機能を実装できます。

Hook 間のデータ受け渡し

ユーザー選択とその詳細情報表示を連携させる例です。

javascriptconst UserManagementPanel = () => {
  const [selectedUserId, setSelectedUserId] =
    useState(null);

  // ユーザー一覧の取得
  const { data: usersData, loading: usersLoading } =
    useQuery(GET_USERS);

  // 選択されたユーザーの詳細情報を遅延取得
  const [
    getUserDetails,
    { data: userDetailsData, loading: detailsLoading },
  ] = useLazyQuery(GET_USER_DETAILS);

  // ユーザー削除用のmutation
  const [deleteUser] = useMutation(DELETE_USER, {
    // 削除後にキャッシュからユーザーを除去
    update: (cache, { data: { deleteUser } }) => {
      const existingUsers = cache.readQuery({
        query: GET_USERS,
      });
      cache.writeQuery({
        query: GET_USERS,
        data: {
          users: existingUsers.users.filter(
            (user) => user.id !== deleteUser.id
          ),
        },
      });
    },
  });

  // ユーザー選択時の処理
  const handleUserSelect = (userId) => {
    setSelectedUserId(userId);
    getUserDetails({ variables: { userId } });
  };

  // ユーザー削除時の処理
  const handleUserDelete = async (userId) => {
    if (window.confirm('本当に削除しますか?')) {
      try {
        await deleteUser({ variables: { userId } });
        // 選択中のユーザーが削除された場合はクリア
        if (selectedUserId === userId) {
          setSelectedUserId(null);
        }
      } catch (error) {
        console.error('削除エラー:', error);
      }
    }
  };

  return (
    <div className='user-management-panel'>
      <div className='user-list-section'>
        <h2>ユーザー一覧</h2>
        {usersLoading ? (
          <div>読み込み中...</div>
        ) : (
          <div className='user-list'>
            {usersData?.users.map((user) => (
              <div
                key={user.id}
                className={`user-item ${
                  selectedUserId === user.id
                    ? 'selected'
                    : ''
                }`}
                onClick={() => handleUserSelect(user.id)}
              >
                <img src={user.avatar} alt={user.name} />
                <div className='user-info'>
                  <h4>{user.name}</h4>
                  <p>{user.email}</p>
                </div>
                <button
                  onClick={(e) => {
                    e.stopPropagation();
                    handleUserDelete(user.id);
                  }}
                  className='delete-button'
                >
                  削除
                </button>
              </div>
            ))}
          </div>
        )}
      </div>

      <div className='user-details-section'>
        <h2>ユーザー詳細</h2>
        {!selectedUserId && (
          <p>左からユーザーを選択してください。</p>
        )}

        {detailsLoading && (
          <div>詳細情報を読み込み中...</div>
        )}

        {userDetailsData?.userDetails && (
          <UserDetailsPanel
            user={userDetailsData.userDetails}
          />
        )}
      </div>
    </div>
  );
};

依存関係のあるクエリ処理

複数のクエリに依存関係がある場合の実装方法です。

javascriptconst ProjectDashboard = ({ projectId }) => {
  // プロジェクト基本情報の取得
  const { data: projectData, loading: projectLoading } =
    useQuery(GET_PROJECT, { variables: { projectId } });

  // プロジェクトのメンバー情報を取得(プロジェクトデータが取得できた後)
  const { data: membersData } = useQuery(
    GET_PROJECT_MEMBERS,
    {
      variables: { projectId },
      skip: !projectData?.project, // プロジェクトデータがない場合はスキップ
    }
  );

  // タスク情報を取得(プロジェクトが存在し、アクティブな場合のみ)
  const { data: tasksData } = useQuery(GET_PROJECT_TASKS, {
    variables: { projectId },
    skip:
      !projectData?.project ||
      projectData.project.status !== 'ACTIVE',
  });

  // 統計情報を遅延取得(ユーザーがタブを開いた時のみ)
  const [
    getProjectStats,
    { data: statsData, loading: statsLoading },
  ] = useLazyQuery(GET_PROJECT_STATS);

  const [activeTab, setActiveTab] = useState('overview');

  // 統計タブが選択された時に統計データを取得
  useEffect(() => {
    if (
      activeTab === 'stats' &&
      projectData?.project &&
      !statsData
    ) {
      getProjectStats({
        variables: {
          projectId,
          dateRange: 'LAST_30_DAYS',
        },
      });
    }
  }, [
    activeTab,
    projectData,
    getProjectStats,
    statsData,
    projectId,
  ]);

  if (projectLoading) {
    return <div>プロジェクト情報を読み込み中...</div>;
  }

  const project = projectData?.project;
  if (!project) {
    return <div>プロジェクトが見つかりません。</div>;
  }

  return (
    <div className='project-dashboard'>
      <div className='dashboard-header'>
        <h1>{project.name}</h1>
        <div className='project-status'>
          <span
            className={`status-badge ${project.status.toLowerCase()}`}
          >
            {project.status}
          </span>
        </div>
      </div>

      <div className='dashboard-tabs'>
        <button
          onClick={() => setActiveTab('overview')}
          className={
            activeTab === 'overview' ? 'active' : ''
          }
        >
          概要
        </button>
        <button
          onClick={() => setActiveTab('members')}
          className={
            activeTab === 'members' ? 'active' : ''
          }
        >
          メンバー (
          {membersData?.projectMembers?.length || 0})
        </button>
        <button
          onClick={() => setActiveTab('tasks')}
          className={activeTab === 'tasks' ? 'active' : ''}
        >
          タスク ({tasksData?.projectTasks?.length || 0})
        </button>
        <button
          onClick={() => setActiveTab('stats')}
          className={activeTab === 'stats' ? 'active' : ''}
        >
          統計
        </button>
      </div>

      <div className='dashboard-content'>
        {activeTab === 'overview' && (
          <ProjectOverview project={project} />
        )}

        {activeTab === 'members' && (
          <ProjectMembers
            members={membersData?.projectMembers}
          />
        )}

        {activeTab === 'tasks' && (
          <ProjectTasks tasks={tasksData?.projectTasks} />
        )}

        {activeTab === 'stats' && (
          <div>
            {statsLoading ? (
              <div>統計データを読み込み中...</div>
            ) : (
              <ProjectStats
                stats={statsData?.projectStats}
              />
            )}
          </div>
        )}
      </div>
    </div>
  );
};

パフォーマンス最適化

キャッシュ戦略

Apollo Client の強力なキャッシュ機能を最大限活用する方法をご説明します。

フェッチポリシーの選択

データの性質に応じて適切なフェッチポリシーを選択することで、パフォーマンスを大幅に改善できます。

javascript// キャッシュ戦略の実装例
const DataFetchingStrategies = () => {
  // 1. キャッシュファーストでユーザープロフィール取得
  const { data: profileData } = useQuery(GET_USER_PROFILE, {
    fetchPolicy: 'cache-first', // キャッシュを優先
    variables: { userId: currentUser.id },
  });

  // 2. ネットワークファーストでダッシュボードデータ取得
  const { data: dashboardData } = useQuery(
    GET_DASHBOARD_DATA,
    {
      fetchPolicy: 'network-only', // 常に最新データを取得
      pollInterval: 30000, // 30秒間隔で自動更新
    }
  );

  // 3. キャッシュアンドネットワークで商品リスト取得
  const { data: productsData, networkStatus } = useQuery(
    GET_PRODUCTS,
    {
      fetchPolicy: 'cache-and-network', // キャッシュを表示しつつネットワークからも取得
      notifyOnNetworkStatusChange: true,
    }
  );

  // 4. ノーキャッシュで検索結果取得
  const [searchProducts] = useLazyQuery(SEARCH_PRODUCTS, {
    fetchPolicy: 'no-cache', // 検索結果はキャッシュしない
  });

  return (
    <div>
      <div className='profile-section'>
        <h2>プロフィール(キャッシュ優先)</h2>
        {profileData && (
          <UserProfile user={profileData.user} />
        )}
      </div>

      <div className='dashboard-section'>
        <h2>ダッシュボード(常に最新)</h2>
        {dashboardData && (
          <Dashboard data={dashboardData.dashboard} />
        )}
      </div>

      <div className='products-section'>
        <h2>商品一覧(キャッシュ+ネットワーク)</h2>
        {networkStatus === NetworkStatus.refetch && (
          <div className='updating-indicator'>
            更新中...
          </div>
        )}
        {productsData && (
          <ProductList products={productsData.products} />
        )}
      </div>
    </div>
  );
};

キャッシュ更新の制御

mutation の実行後に関連するクエリのキャッシュを適切に更新する方法です。

javascriptconst ProductManagement = () => {
  // 商品作成mutation
  const [createProduct] = useMutation(CREATE_PRODUCT, {
    // 作成後にキャッシュを更新
    update: (cache, { data: { createProduct } }) => {
      // 既存の商品リストを取得
      const existingProducts = cache.readQuery({
        query: GET_PRODUCTS,
      });

      // 新しい商品を先頭に追加
      cache.writeQuery({
        query: GET_PRODUCTS,
        data: {
          products: [
            createProduct,
            ...existingProducts.products,
          ],
        },
      });
    },
    // 関連クエリを再実行
    refetchQueries: [
      { query: GET_PRODUCT_CATEGORIES },
      { query: GET_PRODUCT_STATS },
    ],
  });

  // 商品更新mutation
  const [updateProduct] = useMutation(UPDATE_PRODUCT, {
    update: (cache, { data: { updateProduct } }) => {
      // 個別商品のキャッシュを更新
      cache.writeQuery({
        query: GET_PRODUCT,
        variables: { id: updateProduct.id },
        data: { product: updateProduct },
      });

      // リスト内の該当商品も更新
      const existingProducts = cache.readQuery({
        query: GET_PRODUCTS,
      });

      if (existingProducts) {
        const updatedProducts =
          existingProducts.products.map((product) =>
            product.id === updateProduct.id
              ? updateProduct
              : product
          );

        cache.writeQuery({
          query: GET_PRODUCTS,
          data: { products: updatedProducts },
        });
      }
    },
  });

  // 商品削除mutation
  const [deleteProduct] = useMutation(DELETE_PRODUCT, {
    update: (cache, { data: { deleteProduct } }) => {
      // キャッシュから削除
      cache.evict({
        id: cache.identify({
          __typename: 'Product',
          id: deleteProduct.id,
        }),
      });
      cache.gc(); // ガベージコレクション実行
    },
  });

  return (
    <div className='product-management'>
      {/* 商品管理UI */}
    </div>
  );
};

ポーリング・リフェッチ制御

効率的なポーリング実装

必要な時にだけポーリングを行い、リソースを節約する実装例です。

javascriptconst LiveDataComponent = () => {
  const [isLive, setIsLive] = useState(false);
  const [pollingInterval, setPollingInterval] = useState(0);

  const { data, loading, startPolling, stopPolling } =
    useQuery(GET_LIVE_DATA, {
      pollInterval: pollingInterval,
      fetchPolicy: 'network-only', // ライブデータなので常にネットワークから取得
    });

  // ライブモード切り替え
  const toggleLiveMode = () => {
    if (isLive) {
      stopPolling();
      setPollingInterval(0);
      setIsLive(false);
    } else {
      setPollingInterval(5000); // 5秒間隔
      startPolling(5000);
      setIsLive(true);
    }
  };

  // ページがアクティブでない時はポーリング停止
  useEffect(() => {
    const handleVisibilityChange = () => {
      if (document.hidden && isLive) {
        stopPolling();
      } else if (!document.hidden && isLive) {
        startPolling(pollingInterval);
      }
    };

    document.addEventListener(
      'visibilitychange',
      handleVisibilityChange
    );
    return () => {
      document.removeEventListener(
        'visibilitychange',
        handleVisibilityChange
      );
    };
  }, [isLive, pollingInterval, startPolling, stopPolling]);

  return (
    <div className='live-data-component'>
      <div className='controls'>
        <button onClick={toggleLiveMode}>
          {isLive ? 'ライブモード停止' : 'ライブモード開始'}
        </button>
        {isLive && (
          <div className='live-indicator'>
            🔴 LIVE - {pollingInterval / 1000}秒間隔で更新中
          </div>
        )}
      </div>

      <div className='data-display'>
        {loading && <div>データ更新中...</div>}
        {data && (
          <div>
            <h3>リアルタイムデータ</h3>
            <p>
              最終更新:{' '}
              {new Date(
                data.liveData.lastUpdated
              ).toLocaleTimeString()}
            </p>
            <div className='metrics'>
              {data.liveData.metrics.map((metric) => (
                <div key={metric.name} className='metric'>
                  <span className='metric-name'>
                    {metric.name}
                  </span>
                  <span className='metric-value'>
                    {metric.value}
                  </span>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

インテリジェントリフェッチ

条件に基づいて適切なタイミングでデータを再取得する実装です。

javascriptconst SmartRefetchComponent = () => {
  const { data, loading, error, refetch } =
    useQuery(GET_USER_DATA);
  const [lastRefresh, setLastRefresh] = useState(
    Date.now()
  );

  // エラー時の自動リトライ
  useEffect(() => {
    if (error) {
      const retryTimer = setTimeout(() => {
        refetch();
      }, 5000); // 5秒後にリトライ

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

  // フォーカス時の自動更新(5分以上経過している場合)
  useEffect(() => {
    const handleFocus = () => {
      const now = Date.now();
      const fiveMinutes = 5 * 60 * 1000;

      if (now - lastRefresh > fiveMinutes) {
        refetch().then(() => {
          setLastRefresh(now);
        });
      }
    };

    window.addEventListener('focus', handleFocus);
    return () =>
      window.removeEventListener('focus', handleFocus);
  }, [lastRefresh, refetch]);

  // 手動更新
  const handleManualRefresh = () => {
    refetch().then(() => {
      setLastRefresh(Date.now());
    });
  };

  return (
    <div className='smart-refetch-component'>
      <div className='header'>
        <h2>ユーザーデータ</h2>
        <button
          onClick={handleManualRefresh}
          disabled={loading}
          className='refresh-button'
        >
          {loading ? '更新中...' : '手動更新'}
        </button>
      </div>

      {error && (
        <div className='error-banner'>
          エラーが発生しました。5秒後に自動的に再試行します...
        </div>
      )}

      {data && (
        <div className='user-data'>
          <p>
            最終更新:{' '}
            {new Date(lastRefresh).toLocaleString()}
          </p>
          {/* ユーザーデータ表示 */}
        </div>
      )}
    </div>
  );
};

まとめ

Apollo Client Hooks は、React アプリケーションにおけるデータ管理を革新的に改善するツールです。今回ご紹介した内容をまとめますと、以下のようになります。

基本的な Hook の特徴:

  • useQuery: 自動的なデータ取得とキャッシュ管理
  • useMutation: データ更新と楽観的更新の実現
  • useLazyQuery: オンデマンドでのデータ取得制御
  • useSubscription: リアルタイムデータ通信の実装

効果的な活用方法:

  1. 適切な Hook の選択: データの性質と取得タイミングに応じた最適な Hook の使い分け
  2. キャッシュ戦略の最適化: フェッチポリシーとキャッシュ更新の適切な設定
  3. エラーハンドリング: ユーザーフレンドリーなエラー処理の実装
  4. パフォーマンス最適化: ポーリング制御と不要な通信の削減

これらのテクニックを組み合わせることで、高性能で使いやすい Web アプリケーションを構築することができます。特に、複数の Hook を連携させた高度なパターンや、リアルタイム通信の実装は、現代の Web アプリケーションには欠かせない機能となっています。

Apollo Client Hooks を効果的に活用して、より良いユーザーエクスペリエンスを提供する開発を進めていただければと思います。

関連リンク