少数ではありますが、依然として膨大な数のプロジェクトでは、データを再取得することなく、変更に対するインターフェイスの即時反応を提供するために Web ソケットの統合が必要です。
これは重要なことなので、開発エクスペリエンスを向上させるための API を提供するサードパーティ ライブラリについて話したり、比較したりするつもりはありません。
私の目標は、 @microsoft/signalr
NextJs と迅速に統合する方法を示すことです。そして、開発中に直面した問題をどのように解決するか。
皆さんがすでに NextJS プロジェクトをローカルにインストールしてデプロイしていることを願っています。私の場合、バージョンは13.2.4
です。さらに重要なライブラリをいくつか追加しましょう。データ取得用のswr
(バージョン2.1.5
) と、ローカル キャッシュと Web ソケット用の API である@microsoft/signalr
(バージョン7.0.5
) をさらに操作します。
npm install --save @microsoft/signalr swr
まず、単純なfetcher
関数と、REST API から初期データを取得するuseChatData
という新しいフックを作成します。チャットのメッセージのリスト、エラーと読み込み状態を検出するフィールド、キャッシュされたデータを変更できるメソッドmutate
を返します。
// hooks/useChatData.ts import useSWR from 'swr'; type Message = { content: string; createdAt: Date; id: string; }; async function fetcher<TResponse>(url: string, config: RequestInit): Promise<TResponse> { const response = await fetch(url, config); if (!response.ok) { throw response; } return await response.json(); } export const useChatData = () => { const { data, error, isLoading, mutate } = useSWR<Message[]>('OUR_API_URL', fetcher); return { data: data || [], isLoading, isError: error, mutate, }; };
想定どおりに動作することをテストするために、ページ コンポーネントを更新しましょう。上部のフックをインポートし、以下のスニペットのようにそこからデータを抽出します。機能する場合は、レンダリングされたデータが表示されます。ご覧のとおり、非常にシンプルです。
// pages/chat.ts import { useChatData } from 'hooks/useChatData'; const Chat: NextPage = () => { const { data } = useChatData(); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };
次のステップでは、将来のページを Web ソケットに接続し、 NewMessage
イベントをキャッチし、新しいメッセージでキャッシュを更新する必要があります。別のファイルでソケット サービスを構築することから始めることを提案します。
SignalR ドキュメントの例によると、イベントをさらにリッスンするために接続のインスタンスを作成する必要があります。また、重複を防ぐための接続オブジェクトと、接続を開始/停止するための 2 つのヘルパーも追加しました。
// api/socket.ts import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; let connections = {} as { [key: string]: { type: string; connection: HubConnection; started: boolean } }; function createConnection(messageType: string) { const connectionObj = connections[messageType]; if (!connectionObj) { console.log('SOCKET: Registering on server events ', messageType); const connection = new HubConnectionBuilder() .withUrl('API_URL', { logger: LogLevel.Information, withCredentials: false, }) .withAutomaticReconnect() .build(); connections[messageType] = { type: messageType, connection: connection, started: false, }; return connection; } else { return connections[messageType].connection; } } function startConnection(messageType: string) { const connectionObj = connections[messageType]; if (!connectionObj.started) { connectionObj.connection.start().catch(err => console.error('SOCKET: ', err.toString())); connectionObj.started = true; } } function stopConnection(messageType: string) { const connectionObj = connections[messageType]; if (connectionObj) { console.log('SOCKET: Stoping connection ', messageType); connectionObj.connection.stop(); connectionObj.started = false; } } function registerOnServerEvents( messageType: string, callback: (payload: Message) => void, ) { try { const connection = createConnection(messageType); connection.on('NewIncomingMessage', (payload: Message) => { callback(payload); }); connection.onclose(() => stopConnection(messageType)); startConnection(messageType); } catch (error) { console.error('SOCKET: ', error); } } export const socketService = { registerOnServerEvents, stopConnection, };
したがって、ページはコード スニペットのようになります。メッセージのリストを含むdata
フェッチおよび抽出し、レンダリングします。また、上記のuseEffect
NewMessage
イベントを登録し、接続を作成し、バックエンドをリッスンします。
イベントがトリガーされると、フックのmutate
メソッドによって既存のリストが新しいオブジェクトで更新されます。
// pages/chat.ts import { useChatData } from 'hooks/useChatData'; import { socketService } from 'api/socket'; const Chat: NextPage = () => { const { data } = useChatData(); useEffect(() => { socketService.registerOnServerEvents( 'NewMessage', (payload: Message) => { mutate(() => [...data, payload], { revalidate: false }); } ); }, [data]); useEffect(() => { return () => { socketService.stopConnection('NewMessage'); }; }, []); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };
私には良さそうですし、機能します。新しいメッセージがフィードにどのように表示されるかがわかります。明確で理解しやすいため、チャットを使用した基本的な例を選択しました。そしてもちろん、それを独自のロジックに適用します。
いずれかのバージョン ( @microsoft/signalr
) を使用すると、重複の問題が発生しました。これは依存関係配列useEffect
に接続されていました。依存関係が変更されるたびに、 connection.on(event, callback);
コールバックをキャッシュし、何度もトリガーしました。
useEffect(() => { // data equals [] by default (registerOnServerEvents 1 run), // but after initial data fetching it changes (registerOnServerEvents 2 run) // each event changes data and triggers runnning of registerOnServerEvents socketService.registerOnServerEvents( 'NewMessage', // callback cached (payload: Message) => { // mutate called multiple times on each data change mutate(() => [...data, payload], { revalidate: false }); } ); }, [data]); // after getting 3 messages events, we had got 4 messages rendered lol
私たちが見つけた最も迅速で信頼性の高い解決策は、React ref
内にデータのコピーを保持し、将来の更新のためにuseEffect
内でそれを使用することでした。
// pages/chat.ts import { useChatData } from 'hooks/useChatData'; import { socketService } from 'api/socket'; const Chat: NextPage = () => { const { data } = useChatData(); const messagesRef = useRef<Message[]>([]); useEffect(() => { messagesRef.current = chatData; }, [chatData]); useEffect(() => { socketService.registerOnServerEvents( 'NewMessage', (payload: Message) => { const messagesCopy = messagesRef.current.slice(); mutate(() => [...messagesCopy, payload], { revalidate: false }); } ); }, [data]); useEffect(() => { return () => { socketService.stopConnection('NewMessage'); }; }, []); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };
現在、必要な修正がすでに含まれていると思われる@microsoft/signalr
の新しいバージョンを使用しています。とにかく、誰かがこの解決策が役立つと考えて、この回避策を使用した場合、私は幸せになります。結論として、SignalR に関する私の経験は非常に肯定的であり、インストールには特定の依存関係や設定は必要なく、問題なく動作し、ニーズを満たしていると言いたいと思います。