跳至主要內容

如何與 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>
);
}
提示

有關 useEffect 鉤子的使用,可以在 下方 找到一些說明。

子元件可以使用狀態和 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 包含一些可以幫助你的功能。請查看

返回範例清單