はじめに
近年のWebアプリでは、ユーザー同士が即時に情報をやり取りできる「リアルタイム通信」が欠かせません。
たとえば、チャットアプリでのメッセージ送受信、株価や暗号資産の価格表示、通知システムなど、最新の状態をページの再読み込みなしで反映する仕組みがこれにあたります。
通常のHTTP通信は「リクエストを送ってレスポンスを受け取る」一方向の流れですが、WebSocketを使うとサーバーとクライアントの間で常に接続を維持し、双方向のリアルタイム通信が可能になります。
これにより、サーバー側のイベントをすぐにユーザーへ届けたり、複数のユーザー間で即座にデータを共有したりできます。
本記事では、このWebSocketを使ってReactアプリからリアルタイム通信を実装し、シンプルなチャットアプリを作成します。
ページを再読み込みせずにメッセージが反映される動きを通して、リアルタイム通信の仕組みを体験していきましょう。
実装環境
今回の記事では、以下の環境でリアルタイムチャットアプリを作成します。
- Node.js:v23.0.0
- React:v9.0.0
- OS:macOS(M1チップ搭載)
基本的なコマンドは他のOSでも共通して動作しますが、Windows環境の場合は一部のコマンドをPowerShell用に読み替えて実行してください。
WebSocket の基本
WebSocketは、クライアント(ブラウザ)とサーバーの間で常時接続を維持しながら、双方向にデータをやり取りできる仕組みです。
通常のHTTP通信では、クライアントがリクエストを送って初めてサーバーが応答しますが、WebSocketでは接続を一度確立すると、どちらからでも自由にメッセージを送信できます。
通信の流れ
- 接続(Connect):クライアントがサーバーへ接続要求を送信
- メッセージ送受信(Send / Receive):接続が確立した状態で双方向通信
- 切断(Close):通信を終了し、接続を閉じる

主なイベント
イベント名 | 説明 |
---|---|
open | 接続が確立したときに発火します。初回の通信準備が完了した合図です。 |
message | サーバーからメッセージを受け取ったときに発火します。リアルタイム更新の要となるイベントです。 |
close | 接続が切断されたときに発火します。再接続処理などで利用します。 |
error | 通信エラーが発生したときに発火します。接続先のURL誤りなどを検知できます。 |
基本的な使い方
以下はブラウザでWebSocketを利用する最もシンプルな例です。
// サーバーへ接続
const socket = new WebSocket("ws://localhost:8080");
// 接続完了時
socket.addEventListener("open", () => {
console.log("接続しました");
socket.send("こんにちは、サーバー!");
});
// メッセージ受信時
socket.addEventListener("message", (event) => {
console.log("受信:", event.data);
});
// 接続終了時
socket.addEventListener("close", () => {
console.log("接続を終了しました");
});
// エラー発生時
socket.addEventListener("error", (err) => {
console.error("エラー:", err);
});
このように、WebSocketはHTTPと異なり「つながりっぱなし」の通信が可能です。
Reactと組み合わせることで、ページを更新せずともリアルタイムにデータを反映できるアプリを簡単に実現できます。
ReactでWebSocketを使う
Reactでは、useEffect
と useState
を組み合わせることで、WebSocket通信を簡潔に実装できます。
ここでは、サーバーとの接続・受信処理・切断までの基本的な流れを解説します。
実装の流れ
useEffect
でコンポーネントのマウント時にWebSocketへ接続し、アンマウント時に切断する。useState
で受信メッセージを管理し、画面にリアルタイム反映させる。
import { useEffect, useState } from "react";
export default function ChatSample() {
// 受信したメッセージを配列で管理
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
// WebSocketサーバーへ接続
const socket = new WebSocket("ws://localhost:8080");
// サーバーからメッセージを受け取るたびに実行
socket.onmessage = (event) => {
setMessages((prev) => [...prev, event.data]);
};
// コンポーネントのアンマウント時に接続を閉じる
return () => {
socket.close();
};
}, []); // 初回マウント時のみ実行
return (
<div style={{ padding: 16, fontFamily: "sans-serif" }}>
<h2>WebSocketメッセージ一覧</h2>
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
</div>
);
}
new WebSocket("ws://localhost:8080")
WebSocketサーバーへ接続を開始します。socket.onmessage
サーバーからメッセージを受信したときに呼び出されます。
受信内容をsetMessages
で追加し、UIが自動的に更新されます。return () => socket.close()
コンポーネントが破棄されるときに接続をクリーンアップ。
不要な接続が残るのを防ぎます。
簡易チャットアプリの実装(サーバー構築~React連携まで)
以下の手順で「複数ブラウザ間で即時にメッセージが同期」するところまで一気に確認できます。
プロジェクト構成
realtime-chat/
├─ server/ # WebSocketサーバー(Node + ws)
│ ├─ package.json
│ └─ index.js
└─ client/ # Reactクライアント
├─ package.json
└─ src/
├─ main.tsx
└─ App.tsx
サーバー実装(Node.js + ws)
server/package.json
{
"name": "ws-chat-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"ws": "^8.18.0"
}
}
server/index.js
// wsライブラリから WebSocketServer クラスをインポート
import { WebSocketServer } from "ws";
// WebSocketサーバーを待ち受けるポート番号
const PORT = 8080;
// WebSocketサーバーを作成して指定ポートで起動
const wss = new WebSocketServer({ port: PORT });
/**
* すべての接続中クライアントにデータを送信する関数
* (exceptで指定されたクライアント以外に送る)
*/
function broadcast(data, except) {
for (const client of wss.clients) {
// readyState === 1 は「接続中(OPEN)」を意味する
if (client !== except && client.readyState === 1) {
client.send(data);
}
}
}
/**
* クライアントが接続した時に呼ばれるイベント
* `ws` は接続してきたクライアント1人分のソケット
*/
wss.on("connection", (ws) => {
console.log("Client connected");
/**
* クライアントからメッセージを受信した時に呼ばれるイベント
* msgBuf は Buffer 型なので、文字列化して扱う
*/
ws.on("message", (msgBuf) => {
const text = msgBuf.toString();
console.log(`Received: ${text}`);
// 受信したメッセージを全員に配信(送信者も含めてOK)
broadcast(text, null);
});
/**
* クライアントが切断した時に呼ばれるイベント
*/
ws.on("close", () => {
console.log("Client disconnected");
});
/**
* エラー発生時(例:ネットワーク切断など)
*/
ws.on("error", (err) => {
console.error("WebSocket error:", err);
});
});
// サーバー起動ログ
console.log(`WebSocket server listening on ws://localhost:${PORT}`);
起動
cd server
npm i
npm run start
React クライアント実装
client/src/App.tsx
import { useEffect, useMemo, useRef, useState } from "react";
// ===============================
// 型定義:チャットメッセージの1件分
// ===============================
type ChatMsg = {
id: string; // 一意のID
text: string; // メッセージ本文
ts: number; // タイムスタンプ(受信時刻)
};
// ===============================
// メインコンポーネント
// ===============================
export default function App() {
// メッセージ一覧(受信したメッセージを配列で保持)
const [messages, setMessages] = useState<ChatMsg[]>([]);
// 入力欄のテキストを保持
const [input, setInput] = useState("");
// WebSocketインスタンスを保持するためのRef(再レンダーされても値が消えない)
const socketRef = useRef<WebSocket | null>(null);
// ===============================
// WebSocket 接続処理(初回マウント時のみ実行)
// ===============================
useEffect(() => {
// サーバーに接続
const ws = new WebSocket("ws://localhost:8080");
socketRef.current = ws; // Refに保存して他関数でも使えるようにする
// 接続成功時
ws.addEventListener("open", () => {
console.log("connected");
});
// サーバーからメッセージを受信したとき
ws.addEventListener("message", (event) => {
const text = String(event.data ?? "");
setMessages((prev) => [
...prev,
{ id: crypto.randomUUID(), text, ts: Date.now() }, // 新しいメッセージを配列に追加
]);
});
// 接続が閉じられたとき
ws.addEventListener("close", () => {
console.log("closed");
});
// 通信エラー発生時
ws.addEventListener("error", (err) => {
console.error("ws error:", err);
});
// クリーンアップ(アンマウント時に接続を閉じる)
return () => {
ws.close();
};
}, []); // ← [] により初回1回だけ実行される
// ===============================
// メッセージ送信可能かどうかの判定(入力が空でないとき)
// ===============================
const canSend = useMemo(() => input.trim().length > 0, [input]);
// ===============================
// 送信処理(ボタンまたはEnterキー押下時)
// ===============================
const handleSend = () => {
const msg = input.trim();
if (!msg) return;
socketRef.current?.send(msg); // WebSocket経由でサーバーに送信
setInput(""); // 入力欄をリセット
};
// Enterキーで送信するショートカット
const handleKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
if (e.key === "Enter" && canSend) {
handleSend();
}
};
// ===============================
// UI部分(メッセージ一覧・入力欄)
// ===============================
return (
<div style={styles.container}>
<h1 style={styles.title}>リアルタイム チャット</h1>
{/* チャットメッセージ一覧 */}
<div style={styles.chatBox}>
{messages.map((m) => (
<div key={m.id} style={styles.message}>
<span>{m.text}</span>
<time style={styles.time}>
{new Date(m.ts).toLocaleTimeString()}
</time>
</div>
))}
</div>
{/* 入力欄と送信ボタン */}
<div style={styles.inputRow}>
<input
style={styles.input}
placeholder="メッセージを入力..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
/>
<button style={styles.button} onClick={handleSend} disabled={!canSend}>
送信
</button>
</div>
{/* ヒントメッセージ */}
<p style={styles.tip}>
もう一つブラウザ(別ウィンドウ/別タブ)で同じページを開き、どちらかで送信すると
<b>即座にもう一方にも表示</b>されれば成功です。
</p>
</div>
);
}
// ===============================
// スタイル定義(簡易的なCSS)
// ===============================
const styles: Record<string, React.CSSProperties> = {
container: { maxWidth: 720, margin: "40px auto", padding: 16, fontFamily: "sans-serif" },
title: { marginBottom: 16 },
chatBox: {
border: "1px solid #ddd",
borderRadius: 8,
padding: 12,
minHeight: 280,
overflowY: "auto",
background: "#fafafa",
marginBottom: 12,
},
message: {
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
gap: 8,
padding: "6px 8px",
background: "white",
borderRadius: 6,
border: "1px solid #eee",
marginBottom: 8,
},
time: { color: "#888", fontSize: 12 },
inputRow: { display: "flex", gap: 8 },
input: {
flex: 1,
padding: "10px 12px",
borderRadius: 8,
border: "1px solid #ccc",
outline: "none",
},
button: {
padding: "10px 16px",
borderRadius: 8,
border: "1px solid #ccc",
background: "#fff",
cursor: "pointer",
},
tip: { color: "#555", marginTop: 8 },
};
処理の流れ
- コンポーネントがマウントされる
useEffect
が実行され、サーバー(ws://localhost:8080
)に接続。socketRef
に WebSocket インスタンスを保持。
- 接続確立 (
open
イベント)- コンソールに
"connected"
が出力。
- コンソールに
- メッセージ受信 (
message
イベント)- サーバーから届いたデータを
messages
に追加。 - React が再レンダーし、画面にメッセージが表示。
- サーバーから届いたデータを
- メッセージ送信
- 入力欄にテキストを入力し「送信」ボタンまたはEnterキー押下。
handleSend()
でsocket.send()
を実行。- メッセージがサーバーへ送られ、他のクライアントにもブロードキャストされる。
- 別のブラウザで受信確認
- 他タブでも同じWebSocketサーバーに接続しているため、
メッセージが即時に反映される。
- 他タブでも同じWebSocketサーバーに接続しているため、
- コンポーネントがアンマウントされる
useEffect
の return 部分が呼ばれ、WebSocket接続をクローズ。
client/src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
起動
npm run dev
動作確認のポイント
- ブラウザAの入力欄に文字を入れて 送信 → ブラウザBにも即時に表示。
- 逆方向(B → A)でも同様に表示される。

- DevTools の Network → WS タブに、
ws://localhost:8080
が接続中でMessages
が送受信されているログが見える。

まとめ
本記事では、WebSocketの基本構造とReactでの実装方法を通して、リアルタイム通信の仕組みを実際に体験しました。
次のステップとしては、ユーザー名やルーム機能を追加したり、socket.io
を用いた拡張的な通信管理にも挑戦してみましょう。
コメント