【30分で完成】GraphQL × React × TypeScriptで作るGitHubユーザー検索アプリ入門

1. はじめに

React初心者の皆さん、ようこそ!
今回は GraphQL × React × TypeScript を使って「GitHubユーザー検索アプリ」を作ります。

本記事は、ただコードを写すだけではなく

  • なぜそのコードが必要なのか(思考プロセス)
  • ReactやTypeScriptの書き方のポイント(記法解説)

を両方押さえることを目的としています。

完成まで30分

記事を読み進めながら手を動かせば、GraphQL API連携とReactのProps/Stateの基礎が一度に学べます。


2. アプリ概要と完成イメージ

作るアプリの機能はシンプルです。

  1. ユーザー名を入力して検索ボタンを押す
  2. GitHub GraphQL APIを使って該当ユーザーを取得
  3. アバターとユーザー名を一覧で表示

3. 開発環境の準備

Node.js と npm の確認

node -v
npm -v

どちらもバージョンが表示されればOKです。


ViteでReact+TypeScriptプロジェクト作成

npm create vite@latest github-user-search -- --template react-ts
cd github-user-search
npm install

必要なライブラリを追加

npm install @apollo/client graphql

4. 思考プロセス:設計とデータの流れ

Reactアプリを作るときは、まず「どのコンポーネントが何を担当するか」を決めておくと、後の実装がスムーズになります。今回は以下の3つに分割します。

  • App.tsx(親コンポーネント)
    • 検索キーワードの状態管理
    • GraphQLクエリの実行(Apollo Client)
    • 子コンポーネントへのデータ渡し
  • SearchForm.tsx(子コンポーネント)
    • 入力欄と検索ボタンのUI
    • 入力内容を親に渡すためのイベント処理
  • UserList.tsx(子コンポーネント)
    • 検索結果(ユーザー一覧)の表示
    • 各ユーザー情報のレイアウト

データの流れ

  1. ユーザーが SearchForm でキーワードを入力し、検索ボタンをクリック。
  2. SearchForm から入力値が App に渡される。
  3. App が GraphQL クエリを実行し、GitHub API からユーザー情報を取得。
  4. 取得結果を UserList に渡し、リストとして表示。

この流れにより、「状態は親で管理」し、子は「見た目とイベントのみ」を担当するシンプルな構造になります。


ディレクトリ構成

src/
├── components/
│ ├── SearchForm.tsx
│ └── UserList.tsx
├── apolloClient.ts
├── queries.ts
├── App.tsx
└── main.tsx
  • components/ フォルダには再利用可能なUI部品(子コンポーネント)をまとめます。
  • apolloClient.ts はApollo Clientの設定。
  • queries.ts はGraphQLクエリの定義。
  • App.tsx はアプリの中核(状態・データ取得)。
  • main.tsx はReactアプリのエントリーポイント。

5. GraphQLとGitHub APIの準備

GitHub Personal Access Tokenの取得

  1. GitHubにログイン
  2. [Settings] → [Developer settings] → [Personal Access Tokens]
  3. New Token を作成(read:user 権限を付与)
  4. 発行されたトークンをコピー

.env ファイルの作成

取得したトークンはソースコードに直接書かず、環境変数で管理します。
プロジェクトのルートディレクトリに .env ファイルを作成し、以下のように記述してください。

VITE_GITHUB_TOKEN=取得したトークン

VITE_ から始まる変数名にすると、Vite経由でReactアプリから参照可能になります。
また、.envGit管理対象外 にするため、.gitignore に追加してください。


6. Apollo Clientのセットアップ

コード全文

src/apolloClient.ts

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

const GITHUB_API_URL = "https://api.github.com/graphql";
const GITHUB_TOKEN = import.meta.env.VITE_GITHUB_TOKEN;

export const client = new ApolloClient({
uri: GITHUB_API_URL,
cache: new InMemoryCache(),
headers: {
Authorization: `Bearer ${GITHUB_TOKEN}`,
},
});

解説ポイント

1. インポート

import { ApolloClient, InMemoryCache } from "@apollo/client";
  • ApolloClient:GraphQL サーバーとの通信・キャッシュ・エラーハンドリングを担うクライアント本体。
  • InMemoryCache:取得したデータをメモリ上にキャッシュする仕組み。再取得の削減や UI の即時反映に効きます。

2. API エンドポイントとトークン

const GITHUB_API_URL = "https://api.github.com/graphql";
const GITHUB_TOKEN = import.meta.env.VITE_GITHUB_TOKEN;
  • GITHUB_API_URL は GitHub の GraphQL エンドポイント。
  • GITHUB_TOKEN.env に保存した 環境変数 を参照(Vite では VITE_ プレフィックスが必須)。

3. クライアント生成

export const client = new ApolloClient({
uri: GITHUB_API_URL,
cache: new InMemoryCache(),
headers: {
Authorization: `Bearer ${GITHUB_TOKEN}`,
},
});
  • uri:通信先の GraphQL サーバー(ここでは GitHub)。
  • cachenew InMemoryCache() を指定すると、同じクエリ結果をキャッシュから即座に再利用できます。
  • headers.AuthorizationBearer <token> 形式で認証ヘッダを付与。GitHub GraphQL API は認証必須です。

トークン未設定だと 401 Unauthorized が発生します。
トークン権限は read:user 程度で十分です(必要に応じて拡張)。


7. GraphQLクエリの定義

コード全文

src/queries.ts

import { gql } from "@apollo/client";

export const SEARCH_USERS = gql`
query SearchUsers($query: String!) {
search(query: $query, type: USER, first: 10) {
nodes {
... on User {
id
login
avatarUrl
}
}
}
}
`;

解説ポイント

1.gql タグでクエリを定義

import { gql } from "@apollo/client";
  • gqlGraphQL 文法を文字列内で表すためのタグ関数。Apollo がパースしてクエリ AST に変換します。

2. 変数付きクエリの宣言

query SearchUsers($query: String!) { ... }
  • SearchUsers はクエリの名前(任意)で、デバッグ時に特定しやすくなります。
  • $query: String! は「必須の文字列型の変数 $query」を受け取るという意味。
    実行時に { variables: { query: "keyword" } } のように渡します。

3. GitHub 検索フィールド search

search(query: $query, type: USER, first: 10) { ... }
  • query: $query:ユーザーが入力した検索キーワード。
  • type: USER:検索対象を「ユーザー」に限定。
  • first: 10:最初の 10 件を取得(ページネーションの一種)。
    もっと欲しいときは after 引数を加えて次ページを取得可能(pageInfo { endCursor hasNextPage } を展開)。

4. nodes と型分岐(インラインフラグメント)

nodes {
... on User {
id
login
avatarUrl
}
}
  • search は複数の型(Issue, Repository, User など)を返しうるため、... on User で「User のときだけこれらのフィールドを取得して」と指定します。
  • 取得しているのは UI 表示に必要な最小限:
    • id:React の key に使える一意 ID
    • login:ユーザー名
    • avatarUrl:アバター画像 URL

名前や Bio、フォロワー数なども必要なら name, bio, followers { totalCount } を追加できます。必要なものだけを明示して取得するのが GraphQL の思想です。


8. App.tsx(親コンポーネント)

コード全文

src/App.tsx

import React, { useState } from "react";
import { ApolloProvider, useLazyQuery } from "@apollo/client";
import { client } from "./apolloClient";
import { SEARCH_USERS } from "./queries";
import SearchForm from "./components/SearchForm";
import UserList from "./components/UserList";

function App() {
return (
<ApolloProvider client={client}>
<InnerApp />
</ApolloProvider>
);
}

function InnerApp() {
const [searchUsers, { loading, error }] = useLazyQuery(SEARCH_USERS);
const [users, setUsers] = useState<any[]>([]);

const handleSearch = (keyword: string) => {
searchUsers({ variables: { query: keyword } }).then((res) => {
setUsers(res.data?.search?.nodes ?? []);
});
};

return (
<div style={{ minHeight: "100vh", padding: 24 }}>
<h1>GitHubユーザー検索</h1>
<SearchForm onSearch={handleSearch} />
{loading && <p>読み込み中...</p>}
{error && <p>エラーが発生しました</p>}
<UserList users={users} />
</div>
);
}

export default App;

解説ポイント

1. useLazyQueryの使い方

const [searchUsers, { data, loading, error }] = useLazyQuery(SEARCH_USERS);
  • useLazyQuery は、コンポーネント描画時ではなく「特定のタイミングで」クエリを実行したいときに使います。
  • 第一要素 searchUsers はクエリを発火する関数。
  • 第二要素は { data, loading, error } というオブジェクトで、クエリの状態を保持します。

2. 親から子へ関数を渡す

<SearchForm onSearch={handleSearch} />
  • SearchForm は、ユーザーが入力した検索ワードを親に伝えるためのProps onSearch を受け取ります。
  • Reactでは「子→親」方向のデータ伝達は直接できないため、このように「親から関数を渡し、子がそれを呼び出す」という形をとります。

3. ApolloProviderで全体をラップ

<ApolloProvider client={client}> ... </ApolloProvider>
  • ApolloProvider はReactのコンテキストAPIを利用して、子コンポーネント全体でApollo Clientのインスタンスを利用可能にします。
  • client は先ほど作成した apolloClient.ts の設定を使っています。

9. SearchForm.tsx(検索フォーム)

コード全文

src/components/SearchForm.tsx

import React, { useState } from "react";

type Props = {
onSearch: (keyword: string) => void;
};

function SearchForm({ onSearch }: Props) {
const [input, setInput] = useState("");

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
onSearch(input);
};

return (
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="GitHubユーザー名"
/>
<button type="submit">検索</button>
</form>
);
}

export default SearchForm;

解説ポイント

1. Propsの型定義

type Props = {
onSearch: (keyword: string) => void;
};
  • 親から渡される onSearch 関数の型を定義。
  • 引数は string(検索キーワード)、戻り値は void(値を返さない)です。

2. useStateで入力欄の状態を管理

const [input, setInput] = useState("");
  • 入力欄の値はコンポーネントのstateで管理します(Controlled Component)。
  • input が現在の入力値、setInput が値を更新する関数。

3. フォーム送信イベントの制御

e.preventDefault();
  • HTMLフォームはデフォルトで送信時にページをリロードします。これを防ぐためにpreventDefault()を呼びます。

10. UserList.tsx(結果表示)

コード全文

src/components/UserList.tsx

import React from "react";

type User = {
id: string;
login: string;
avatarUrl: string;
};

type Props = {
users: User[];
};

function UserList({ users }: Props) {
if (users.length === 0) return <p>ユーザーが見つかりません</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>
<img src={user.avatarUrl} alt={user.login} width={50} />
<span>{user.login}</span>
</li>
))}
</ul>
);
}

export default UserList;

解説ポイント

1. 配列のmapでリストを描画

users.map((user) => ( ... ))
  • 配列の各要素を<li>に変換し、リストを生成します。
  • Reactでは配列レンダリング時に必ずkey属性を付与する必要があります。

2. ユーザーがいない場合の条件分岐

if (users.length === 0) return <p>ユーザーが見つかりません</p>;
  • 検索結果が空配列の場合は、リストの代わりにメッセージを表示します。

11. 実行と動作確認

npm run dev

ブラウザで http://localhost:5173 を開き、
ユーザー名(例: testuser)を検索するとGitHubユーザーが表示されます。


まとめ

ここまでで、GitHubユーザー検索アプリが完成しました。

最初に構成を見たときは、「コンポーネント分割」「GraphQL」「Apollo Client」…と聞き慣れない単語が多く、少し腰が引けた方もいるかもしれません。

でも、実際に手を動かしてみると、「あれ、思ったよりシンプルかも」と感じたはずです。

今回の作業で身についたポイントは、ざっくりこんなところです。

  • 親子コンポーネント間でのデータ受け渡し(Props)の仕組み
  • GraphQLクエリの基本構造と、必要なデータだけを取ってくる発想
  • Apollo Clientを使ったAPI連携の流れ
  • useLazyQueryで“必要なときだけ”データを取得する方法

個人的に、このアプリを作っていて改めて感じたのは、
「状態は親で管理し、見た目は子が担当する」というシンプルな設計ルールの強さです。

このおかげで、処理の流れがとても追いやすくなりましたし、コードの見通しも良くなりました。

もし余裕があれば、今回のアプリをベースに「ユーザーの詳細ページを追加する」「ページネーションを付ける」など、ちょっとした拡張にも挑戦してみてください!

関連記事