はじめに|ReactでOpenAI APIを利用したアプリを作る理由

今回は React から OpenAI API を直接呼び出して、シンプルなチャットアプリを作る チュートリアルです。
ゴールは「難しいライブラリや仕組みは使わず、まずは基礎を理解する」こと!
この記事で体験できることは2つです。
- React の基本(state 管理、イベント処理の流れ)
- OpenAI API の呼び出し方(fetch を使ったリクエスト)
まずは最小限のコードで “AIと会話できた” という感覚を掴んでいきましょう。
書いたコードは超細かに解説しているのでReact初心者の方も安心して読み進めてください!
React × OpenAI APIで作るチャットアプリの完成イメージ
- 画面にはチャットログが並ぶ
- 入力欄に文章を入れて「送信」すると AI が返答してくれる
- React の基礎と API 呼び出しの両方が学べる

0. Reactチャットアプリを作る前に必要な事前準備
- Node.js(18以上推奨)
- Vite
上記準備できていない方は以下の記事を参考にインストールしてみてください!
Reactを5分で試せる!初心者向けReact 開発環境構築ガイド
- OpenAI API キー
- OpenAI公式にログイン → API Keys ページ
- 「Create new secret key」で発行
- sk-で始まる文字列をコピーしておきましょう
 
1. React + TypeScript環境でチャットアプリ用プロジェクトを作成する
まずは React + TypeScript の環境を作成します。
npm create vite@latest ai-chat -- --template react-ts
cd ai-chat
npm install- vite… 最新のフロントエンド開発環境を作るツール
- react-ts… React + TypeScript のテンプレートを指定
2. ReactプロジェクトにOpenAI APIキーを設定する方法
Vite では環境変数をフロントエンドで使うとき、必ず VITE_ プレフィックスが必要です。
プロジェクト直下に .env ファイルを作り、以下を記載してください。
VITE_OPENAI_API_KEY=sk-ここに自分のAPIキー3. ReactでチャットUIを実装する(Chatコンポーネントの作成)
ここが本体です。src/Chat.tsx を新規作成し、以下のコードを貼り付けてください。
import { useEffect, useRef, useState } from "react";
type Msg = { role: "user" | "assistant"; content: string };
export default function Chat() {
  const [messages, setMessages] = useState<Msg[]>([]);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const [err, setErr] = useState<string | null>(null);
  const endRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    endRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages, loading]);
  async function send() {
    const text = input.trim();
    if (!text || loading) return;
    const next = [...messages, { role: "user", content: text }];
    setMessages(next);
    setInput("");
    setErr(null);
    setLoading(true);
    try {
      const res = await fetch("https://api.openai.com/v1/chat/completions", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${import.meta.env.VITE_OPENAI_API_KEY}`,
        },
        body: JSON.stringify({
          model: "gpt-4o-mini",
          messages: [
            { role: "system", content: "あなたは親切な日本語アシスタントです。" },
            ...next,
          ],
        }),
      });
      if (!res.ok) {
        const t = await res.text();
        throw new Error(`${res.status} ${res.statusText}\n${t}`);
      }
      const json = await res.json();
      const reply: string = json.choices?.[0]?.message?.content ?? "(応答なし)";
      setMessages((m) => [...m, { role: "assistant", content: reply }]);
    } catch (e: any) {
      setErr(e?.message ?? "エラーが発生しました");
    } finally {
      setLoading(false);
    }
  }
  function onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      send();
    }
  }
  return (
    <div style={{ maxWidth: 720, margin: "40px auto", padding: "0 16px" }}>
      <h1>React × OpenAI シンプルチャット</h1>
      <div style={{
        color: "#333",
        border: "1px solid #ddd",
        borderRadius: 8,
        padding: 16,
        minHeight: 320,
        background: "#fafafa"
      }}>
        {messages.map((m, i) => (
          <div key={i} style={{ whiteSpace: "pre-wrap", margin: "10px 0" }}>
            <b>{m.role === "user" ? "You" : "AI"}:</b> {m.content}
          </div>
        ))}
        {loading && <div>…考え中</div>}
        <div ref={endRef} />
      </div>
      {err && <div style={{ color: "#b00020", marginTop: 8 }}>エラー: {err}</div>}
      <div style={{ display: "flex", gap: 8, marginTop: 12 }}>
        <textarea
          rows={3}
          style={{ flex: 1 }}
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={onKeyDown}
          placeholder="質問を入力(Enterで送信 / Shift+Enterで改行)"
        />
        <button onClick={send} disabled={loading || !input.trim()}>
          {loading ? "送信中…" : "送信"}
        </button>
      </div>
    </div>
  );
}コード解説(不要な方は飛ばしてください!)
ReactのuseStateとuseRefで状態管理と参照を扱う
const [messages, setMessages] = useState<Msg[]>([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
const endRef = useRef<HTMLDivElement>(null);- useState<T>(初期値)
- React の 状態(state) を作るフック。
- 戻り値は タプル(配列風の2要素)で、[現在の値, 更新関数]の形。
- useState<Msg[]>([])の- <Msg[]>は ジェネリクス。- messagesが「- Msg型の配列」だと 型で約束 しています。
- setMessages(newValue)を呼ぶと、再レンダリング が起きて UI が最新状態に同期します。
 
- messages… 会話ログ(- { role: "user" | "assistant"; content: string }の配列)。
- setMessages…- messagesを更新する関数。元配列を直接書き換えず、新しい配列を渡すのが React の鉄則(不変性/イミュータブル)。
- input… 入力中の文字列。テキストエリアの- valueと連動(Controlled Component)。
- loading… API 呼び出し中かどうかのフラグ。二重送信の抑止やローディング表示に使います。
- err… エラーメッセージ。- string | nullという ユニオン型。エラーがなければ- null。
- useRef<HTMLDivElement>(null)
- DOM への参照を保持するフック。<HTMLDivElement>は 参照する要素の型。
- endRef.currentで実際の要素を取り出せます。値を変えても再レンダリングは発生しないのが- useRefの特徴。
- 初期値 nullにしておき、ref={endRef}を付けた要素がマウントされたら自動的に入ります。
 
- DOM への参照を保持するフック。
useEffectでチャットログを自動スクロールする
useEffect(() => {
  endRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, loading]);- useEffect(副作用関数, 依存配列)
- レンダリング後に実行したい処理を書く場所(副作用=サイドエフェクト)。
- ここでは「ログが増えたり、読み込み完了したときに最下部までスクロールする」副作用を実行。
 
- endRef.current?.scrollIntoView(…)
- ?.は オプショナルチェイニング。- currentが- nullではない時だけメソッドを呼びます(初回レンダリング時の安全策)。
- scrollIntoView({ behavior: "smooth" })はブラウザ組み込みの API。対象の要素が見える位置までスクロールします。- "smooth"でアニメーション。
 
- 依存配列 [messages, loading]
- ここに挙げた値が 変わったときだけ 副作用が走る。
- 新しいメッセージが追加されたり、loadingがtrue→falseに変わった時に、ログの末尾へ自動スクロールします。
 
OpenAI API へのリクエスト
const res = await fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${import.meta.env.VITE_OPENAI_API_KEY}`,
  },
  body: JSON.stringify({
    model: "gpt-4o-mini",
    messages: [
      { role: "system", content: "あなたは親切な日本語アシスタントです。" },
      ...next,
    ],
  }),
});- fetch(url, options)
- ブラウザの HTTP クライアント関数。awaitでレスポンスを待ちます。
- method: "POST"と- headers、- body(JSON文字列)を指定して POST リクエストを作成。
 
- ブラウザの HTTP クライアント関数。
- Authorization: Bearer …
- OpenAI API へは Bearer トークンで認証。
- import.meta.env.VITE_OPENAI_API_KEYは Vite の環境変数。- .envに- VITE_で始まるキー名で書いた値だけがクライアントから参照できます。
- .env更新後は 開発サーバーの再起動が必要 です。
 
- body: JSON.stringify({…})
- fetchは 文字列を送るので、オブジェクトを JSON 文字列に変換します。
- ここでは Chat Completions の仕様に従い、modelとmessagesを渡しています。
 
- model: “gpt-4o-mini”
- 軽量で安価、応答も速く、学習用にちょうど良いバランスのモデル。
- 必要に応じて他モデルにも差し替え可。
 
- messages(会話の履歴)
- 配列で、各要素が { role, content }の形。
- roleは- "system" | "user" | "assistant"のいずれか。- system… AI の性格付け。「日本語で簡潔に」「誤情報を避ける」など。
- user… ユーザーからの発言。
- assistant… AI の過去の返答(履歴を維持したいときに含める)。
 
- ...nextは スプレッド構文。- nextは- const next = [...messages, { role: "user", content: text }];の形で作った 最新の履歴。
- 「これまでのログ」+「今入力したユーザー発言(role: "user")」が展開され、API に渡ります。
 
 
- 配列で、各要素が 
Enterで送信 / Shift+Enterで改行
function onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
  if (e.key === "Enter" && !e.shiftKey) {
    e.preventDefault();
    send();
  }
}- イベント型:React.KeyboardEvent<HTMLTextAreaElement>- TypeScript のジェネリクスで「どの要素からのキーイベントか」を明示。補完が効くようになり安全です。
 
- e.key === "Enter"- 押されたキーが Enter かどうか。
 
- !e.shiftKey
- Shift キーが押されていないときだけ実行 → Enter 単独で送信。
- 逆に Shift+Enter のときは returnせず ブラウザのデフォルト(改行) を利用。
 
- e.preventDefault()- Enter で送信する瞬間、テキストエリアの デフォルトの改行 を キャンセル。
- これがないと「送信」と同時に改行も入ってしまいます。
 
- 操作感のまとめ
- Enter → 送信
- Shift+Enter → 改行(段落を分けたいときに使う定番の挙動)
 
4. Reactアプリ全体にChatコンポーネントを組み込む
次に、src/App.tsx を編集してチャットを表示しましょう。
import Chat from "./Chat";
function App() {
  return <Chat />;
}
export default App;これでアプリ全体に Chat コンポーネントが組み込まれます。
5. Reactチャットアプリを実際に動かしてOpenAI APIと会話する
開発サーバーを起動します。
npm run devブラウザで http://localhost:5173 を開きましょう。

テキストを入力して送信してみます。

少し待つと…

回答が返ってきました!!
まとめ|React × OpenAI APIでチャットアプリを作って学べること

今回は 「React × OpenAI API」 を使って、シンプルなチャットアプリを作りました。
- Reactの基本:状態を管理するとUIが自動で更新される
- OpenAI APIの基本:fetchで呼び出すだけで会話ができる
- 難しいライブラリは不要。まずは「動かす」体験を大事に!
ここまでで「自分の手でAIを動かせた」という感覚を持てれば大成功です。
次のステップでは、UIを整えたり、過去の会話を保存したりして発展させていきましょう!















コメントを残す