如何與 React 搭配使用
本指南說明如何在 React 應用程式中使用 Socket.IO。
範例
結構
src
├── App.js
├── components
│ ├── ConnectionManager.js
│ ├── ConnectionState.js
│ ├── Events.js
│ └── MyForm.js
└── socket.js
Socket.IO 客戶端在 src/socket.js
檔案中初始化
src/socket.js
import { io } from 'socket.io-client';
// "undefined" means the URL will be computed from the `window.location` object
const URL = process.env.NODE_ENV === 'production' ? undefined : 'http://localhost:4000';
export const socket = io(URL);
預設情況下,Socket.IO 客戶端會立即開啟與伺服器的連線。你可以使用 autoConnect
選項來防止這種行為
export const socket = io(URL, {
autoConnect: false
});
在這種情況下,你需要呼叫 socket.connect()
來讓 Socket.IO 客戶端連線。例如,當使用者在連線前必須提供某種憑證時,這會很有用。
在開發期間,您需要在伺服器上啟用 CORS
const io = new Server({
cors: {
origin: "http://localhost:3000"
}
});
io.listen(4000);
參考:處理 CORS
事件監聽器會在 App
元件中註冊,此元件會儲存狀態,並透過 props 將狀態傳遞給其子元件。
另請參閱:https://react.dev.org.tw/learn/sharing-state-between-components
src/App.js
import React, { useState, useEffect } from 'react';
import { socket } from './socket';
import { ConnectionState } from './components/ConnectionState';
import { ConnectionManager } from './components/ConnectionManager';
import { Events } from "./components/Events";
import { MyForm } from './components/MyForm';
export default function App() {
const [isConnected, setIsConnected] = useState(socket.connected);
const [fooEvents, setFooEvents] = useState([]);
useEffect(() => {
function onConnect() {
setIsConnected(true);
}
function onDisconnect() {
setIsConnected(false);
}
function onFooEvent(value) {
setFooEvents(previous => [...previous, value]);
}
socket.on('connect', onConnect);
socket.on('disconnect', onDisconnect);
socket.on('foo', onFooEvent);
return () => {
socket.off('connect', onConnect);
socket.off('disconnect', onDisconnect);
socket.off('foo', onFooEvent);
};
}, []);
return (
<div className="App">
<ConnectionState isConnected={ isConnected } />
<Events events={ fooEvents } />
<ConnectionManager />
<MyForm />
</div>
);
}
子元件可以使用狀態和 socket
物件,如下所示
src/components/ConnectionState.js
import React from 'react';
export function ConnectionState({ isConnected }) {
return <p>State: { '' + isConnected }</p>;
}
src/components/Events.js
import React from 'react';
export function Events({ events }) {
return (
<ul>
{
events.map((event, index) =>
<li key={ index }>{ event }</li>
)
}
</ul>
);
}
src/components/ConnectionManager.js
import React from 'react';
import { socket } from '../socket';
export function ConnectionManager() {
function connect() {
socket.connect();
}
function disconnect() {
socket.disconnect();
}
return (
<>
<button onClick={ connect }>Connect</button>
<button onClick={ disconnect }>Disconnect</button>
</>
);
}
src/components/MyForm.js
import React, { useState } from 'react';
import { socket } from '../socket';
export function MyForm() {
const [value, setValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
function onSubmit(event) {
event.preventDefault();
setIsLoading(true);
socket.timeout(5000).emit('create-something', value, () => {
setIsLoading(false);
});
}
return (
<form onSubmit={ onSubmit }>
<input onChange={ e => setValue(e.target.value) } />
<button type="submit" disabled={ isLoading }>Submit</button>
</form>
);
}
有關 useEffect
鉤子的說明
清除
在設定函式中註冊的任何事件監聽器都必須在清除回呼中移除,以避免重複的事件註冊。
useEffect(() => {
function onFooEvent(value) {
// ...
}
socket.on('foo', onFooEvent);
return () => {
// BAD: missing event registration cleanup
};
}, []);
此外,事件監聽器是命名函式,因此呼叫 socket.off()
只會移除這個特定的監聽器
useEffect(() => {
socket.on('foo', (value) => {
// ...
});
return () => {
// BAD: this will remove all listeners for the 'foo' event, which may
// include the ones registered in another component
socket.off('foo');
};
}, []);
相依性
onFooEvent
函式也可以這樣寫
useEffect(() => {
function onFooEvent(value) {
setFooEvents(fooEvents.concat(value));
}
socket.on('foo', onFooEvent);
return () => {
socket.off('foo', onFooEvent);
};
}, [fooEvents]);
這也可以,但請注意,在這種情況下,onFooEvent
監聽器會在每次重新渲染時註銷然後重新註冊。
中斷連線
如果您需要在元件卸載時關閉 Socket.IO 用戶端(例如,如果連線只在應用程式的特定部分需要),您應該
- 確保在設定階段呼叫
socket.connect()
useEffect(() => {
// no-op if the socket is already connected
socket.connect();
return () => {
socket.disconnect();
};
}, []);
在 嚴格模式 中,每個 Effect 會執行兩次,以便在開發期間找出錯誤,因此您會看到
- 設定:
socket.connect()
- 清理:
socket.disconnect()
- 設定:
socket.connect()
- 此 Effect 沒有依賴關係,以防止每次重新整理時重新連接
useEffect(() => {
socket.connect();
function onFooEvent(value) {
setFooEvents(fooEvents.concat(value));
}
socket.on('foo', onFooEvent);
return () => {
socket.off('foo', onFooEvent);
// BAD: the Socket.IO client will reconnect every time the fooEvents array
// is updated
socket.disconnect();
};
}, [fooEvents]);
你可以使用兩個 Effect
import React, { useState, useEffect } from 'react';
import { socket } from './socket';
function App() {
const [fooEvents, setFooEvents] = useState([]);
useEffect(() => {
// no-op if the socket is already connected
socket.connect();
return () => {
socket.disconnect();
};
}, []);
useEffect(() => {
function onFooEvent(value) {
setFooEvents(fooEvents.concat(value));
}
socket.on('foo', onFooEvent);
return () => {
socket.off('foo', onFooEvent);
};
}, [fooEvents]);
// ...
}
重要事項
這些注意事項適用於任何前端架構。
熱模組重新載入
包含 Socket.IO 客戶端初始化的檔案(例如,上方範例中的 src/socket.js
檔案)的熱重新載入可能會讓先前的 Socket.IO 連線保持運作,這表示
- 你的 Socket.IO 伺服器上可能有多個連線
- 你可能會收到來自先前連線的事件
唯一已知的解決方法是在更新這個特定檔案時執行**全頁重新載入**(或完全停用熱重新載入,但這可能會有點極端)。
參考:https://webpack.dev.org.tw/concepts/hot-module-replacement/
子元件中的監聽器
我們強烈建議不要在子元件中註冊事件監聽器,因為它會將 UI 的狀態與事件接收時間綁定在一起:如果元件未裝載,則可能會遺漏一些訊息。
src/components/MyComponent.js
import React from 'react';
export default function MyComponent() {
const [fooEvents, setFooEvents] = useState([]);
useEffect(() => {
function onFooEvent(value) {
setFooEvents(previous => [...previous, value]);
}
// BAD: this ties the state of the UI with the time of reception of the
// 'foo' events
socket.on('foo', onFooEvent);
return () => {
socket.off('foo', onFooEvent);
};
}, []);
// ...
}
暫時斷線
雖然 WebSocket 連線非常強大,但並非總是處於運作狀態
- 使用者與 Socket.IO 伺服器之間的任何事物都可能遭遇暫時故障或重新啟動
- 伺服器本身可能會在自動調整規模政策下被終止
- 在行動瀏覽器的案例中,使用者可能會失去連線或從 Wi-Fi 切換到 4G
這表示你需要妥善處理暫時斷線,才能為使用者提供絕佳的體驗。
好消息是 Socket.IO 包含一些可以幫助你的功能。請查看