はじめに|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を整えたり、過去の会話を保存したりして発展させていきましょう!
コメントを残す