Skip to content

Commit ffc0f7f

Browse files
committed
[IMPL] useWebSocket hook
1 parent 314126f commit ffc0f7f

13 files changed

Lines changed: 285 additions & 41 deletions

File tree

apps/react-tools-demo/src/components/hooks/useWebSocket/useWebSocket.md

Whitespace-only changes.

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ export const COMPONENTS = [
109109
"useAnimation",
110110
"useAudio",
111111
"useVideo",
112-
"useEventSource"
112+
"useEventSource",
113+
"useWebSocket"
113114
]
114115
],
115116
//UTILS

apps/react-tools-demo/src/markdown/useEventSource.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,27 @@ useEventSource<T>({ url, opts, events, immediateConnection, onOpen, onError, onM
1111
>
1212
> - __param__: _UseEventSourceProps_
1313
object
14-
> - __param.url__: _UseEventSourceProps_
14+
> - __param.url?__: _string|URL_
1515
string that represents the location of the remote resource serving the events/messages.
16-
> - __param.opts?__: _UseEventSourceProps_
16+
> - __param.opts?__: _EventSourceInit_
1717
options to configure the new connection. The possible entries are: __withCredentials__ -> boolean value, defaulting to false, indicating if CORS should be set to include credentials.
18-
> - __param.events?__: _UseEventSourceProps_
18+
> - __param.events?__: _{name: string, handler?:(evt:MessagEvent)=>void}[]_
1919
array of objects with properties __name__ and __handler__ to listen specified events from source.
20-
> - __param.immediateConnection?__: _UseEventSourceProps_
20+
> - __param.immediateConnection?__: _boolean_
2121
boolean to start connection immediatly.
22-
> - __param.onOpen?__: _UseEventSourceProps_
22+
> - __param.onOpen?__: _(evt: Event)=>void_
2323
function that will be executed when connection is opened.
24-
> - __param.onError?__: _UseEventSourceProps_
24+
> - __param.onError?__: _(evt: Event)=>void_
2525
function that will be executed when an error occurred.
26-
> - __param.onMessage?__: _UseEventSourceProps_
26+
> - __param.onMessage?__: _(evt: MessageEvent<T>)=>void_
2727
function that will be executed when a message from without event arrived.
2828
>
2929
3030
> ### Returns
3131
>
3232
> __result__: _UseEventSourceResult_
3333
> Object with these properties:
34-
> - __status__: string rapresenting eventsource state connection: __READY__ __CONNECTING__ __OPENED__ or __CLOSED__
34+
> - __status__: string rapresenting eventsource state connection: __READY__ __CONNECTING__ __OPENED__ or __CLOSED__.
3535
> - __data__: last data value arrived from eventSource.
3636
> - __open__: function that opens connection.
3737
> - __close__: function that closes connection.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# useWebSocket
2+
Hook for creating and managing a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) connection to a server, as well as for sending and receiving data on the connection.
3+
4+
## API
5+
6+
```tsx
7+
useWebSocket<T = string | ArrayBuffer | Blob> ({ url, protocols, binaryType, onOpen, onMessage, onError, onClose, immediateConnection, bufferingData, autoReconnect }: UseWebSocketProps): UseWebSocketResult<T>
8+
```
9+
10+
> ### Params
11+
>
12+
> - __param__: _UseWebSocketProps_
13+
object
14+
> - __param.url?__: _UseWebSocketProps_
15+
the URL to which to connect; this should be the URL to which the WebSocket server will respond.
16+
> - __param.protocols?__: _UseWebSocketProps_
17+
either a single protocol string or an array of protocol strings. These strings are used to indicate sub-protocols, so that a single server can implement multiple WebSocket sub-protocols.
18+
> - __param.binaryType?__: _UseWebSocketProps_
19+
the type of binary data being received over the WebSocket connection.
20+
> - __param.immediateConnection?__: _UseWebSocketProps_
21+
boolean to open webSocket connection immediatly.
22+
> - __param.onOpen?__: _UseWebSocketProps_
23+
function that will be executed when webSocket connection has been opened.
24+
> - __param.onMessage?__: _UseWebSocketProps_
25+
function that will be executed when message arrived from webSocket.
26+
> - __param.onError?__: _UseWebSocketProps_
27+
function that will be executed when an error occurred.
28+
> - __param.onClose?__: _UseWebSocketProps_
29+
function that will be executed when webSocket connection has been closed.
30+
> - __param.bufferingData?__: _UseWebSocketProps_
31+
boolean that indicates to use a buffer to keep data sent if connection aren't already opened.
32+
> - __param.autoReconnect?__: _UseWebSocketProps_
33+
boolean or object with properties __retries__, __delay__ and __onFailed__. If an error closes connection and its value isn't false or undefined, a connection will be restored every _delay_ milliseconds for __retries__ time: if connection won't be restored __onFailed__ function will be executed if it is present.
34+
>
35+
36+
> ### Returns
37+
>
38+
> __result__: _UseWebSocketResult_
39+
> Object with these properties:
40+
> - __status__: string rapresenting webSocket state connection: __READY__ __CONNECTING__ __OPENED__ or __CLOSED__.
41+
> - __data__: last data value arrived from webSocket.
42+
> - __open__: function that opens connection with optional _url_ param .
43+
> - __send__: function that sends data by webSocket.
44+
> - __close__: function that closes connection with optional _code_ and _reason_ params.
45+
>

packages/react-tools/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,7 @@
112112
- [x] useAudio
113113
- [x] useVideo
114114
- [x] useEventSource
115-
- [ ] useWebSocket (https://vueuse.org/core/useWebSocket/)
116-
- [ ] useFetch (with suspense ???)
117-
- [ ] useAsync
115+
- [x] useWebSocket
118116
- [ ] useMedia
119117
- [ ] useDevicesList (https://vueuse.org/core/useDevicesList/)
120118
- [ ] useDisplayMedia
@@ -123,10 +121,11 @@
123121
- [ ] useMediaStream
124122
- [ ] useObservable — tracks latest value of an Observable
125123
- [ ] usePointerTouchSwipe (https://vueuse.org/core/usePointerSwipe/ https://vueuse.org/core/useSwipe/)
126-
- [ ] useLock - (https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request)
127-
- [ ] useIndexedDB
128124
- [ ] useWebWorker (https://vueuse.org/core/useWebWorker/)
129125
- [ ] useWebWorkerFn (https://vueuse.org/core/useWebWorkerFn/)
126+
- [ ] useIndexedDB
127+
- [ ] useFetch (with suspense ???)
128+
- [ ] useLock - (https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request)
130129
- [ ] useIdleDetection (not work yet. https://developer.mozilla.org/en-US/docs/Web/API/Idle_Detection_API)
131130

132131
- __UTILS__

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,5 @@ export { useRemotePlayback } from './useRemotePlayback';
8989
export { useAnimation } from './useAnimation';
9090
export { useAudio } from './useAudio';
9191
export { useVideo } from './useVideo';
92-
export { useEventSource } from './useEventSource';
92+
export { useEventSource } from './useEventSource';
93+
export { useWebSocket } from './useWebSocket';

packages/react-tools/src/hooks/useEventSource.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { useCallback, useMemo, useRef } from "react";
22
import { UseEventSourceProps, UseEventSourceResult } from "../models";
3-
import { useEffectOnce, useSyncExternalStore } from ".";
3+
import { useSyncExternalStore } from ".";
44

55
/**
66
* **`useEventSource`**: Hook to handle an [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) or [Server-Sent-Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) connection to an HTTP server, which sends events in text/event-stream format.
77
* @param {UseEventSourceProps} param - object
8-
* @param {UseEventSourceProps} param.url - string that represents the location of the remote resource serving the events/messages.
9-
* @param {UseEventSourceProps} [param.opts] - options to configure the new connection. The possible entries are: __withCredentials__ -> boolean value, defaulting to false, indicating if CORS should be set to include credentials.
10-
* @param {UseEventSourceProps} [param.events] - array of objects with properties __name__ and __handler__ to listen specified events from source.
11-
* @param {UseEventSourceProps} [param.immediateConnection] - boolean to start connection immediatly.
12-
* @param {UseEventSourceProps} [param.onOpen] - function that will be executed when connection is opened.
13-
* @param {UseEventSourceProps} [param.onError] - function that will be executed when an error occurred.
14-
* @param {UseEventSourceProps} [param.onMessage] - function that will be executed when a message from without event arrived.
8+
* @param {string|URL} [param.url] - string that represents the location of the remote resource serving the events/messages.
9+
* @param {EventSourceInit} [param.opts] - options to configure the new connection. The possible entries are: __withCredentials__ -> boolean value, defaulting to false, indicating if CORS should be set to include credentials.
10+
* @param {{name: string, handler?:(evt:MessagEvent)=>void}[]} [param.events] - array of objects with properties __name__ and __handler__ to listen specified events from source.
11+
* @param {boolean} [param.immediateConnection] - boolean to start connection immediatly.
12+
* @param {(evt: Event)=>void} [param.onOpen] - function that will be executed when connection is opened.
13+
* @param {(evt: Event)=>void} [param.onError] - function that will be executed when an error occurred.
14+
* @param {(evt: MessageEvent<T>)=>void} [param.onMessage] - function that will be executed when a message from without event arrived.
1515
* @returns {UseEventSourceResult} result
1616
* Object with these properties:
17-
* - __status__: string rapresenting eventsource state connection: __READY__ __CONNECTING__ __OPENED__ or __CLOSED__
17+
* - __status__: string rapresenting eventsource state connection: __READY__ __CONNECTING__ __OPENED__ or __CLOSED__.
1818
* - __data__: last data value arrived from eventSource.
1919
* - __open__: function that opens connection.
2020
* - __close__: function that closes connection.
@@ -34,10 +34,10 @@ export const useEventSource = <T>({ url, opts, events, immediateConnection, onOp
3434
}
3535
}));
3636
const cachedState = useRef<{ status: UseEventSourceResult<T>["status"], data: UseEventSourceResult<T>["data"] }>({
37-
status: immediateConnection ? "CONNECTING" : "READY",
37+
status: immediateConnection && url ? "CONNECTING" : "READY",
3838
data: null
3939
});
40-
if (immediateConnection && !alreadyOpened.current) {
40+
if (url && immediateConnection && !alreadyOpened.current) {
4141
alreadyOpened.current = true;
4242
sourceRef.current = new EventSource(url, opts);
4343
}
@@ -76,10 +76,13 @@ export const useEventSource = <T>({ url, opts, events, immediateConnection, onOp
7676
});
7777
}
7878

79-
const open = useCallback(() => {
80-
sourceRef.current = new EventSource(url, opts);
81-
cachedState.current.status = "CONNECTING";
82-
notifyRef.current && notifyRef.current();
79+
const open = useCallback((urlParam?: UseEventSourceProps["url"]) => {
80+
if (url || urlParam) {
81+
const urlES = url ?? urlParam as string | URL;
82+
sourceRef.current = new EventSource(urlES, opts);
83+
cachedState.current.status = "CONNECTING";
84+
notifyRef.current && notifyRef.current();
85+
}
8386
}, [opts, url]);
8487

8588
const close = useCallback(() => {
@@ -98,7 +101,7 @@ export const useEventSource = <T>({ url, opts, events, immediateConnection, onOp
98101
}, []),
99102
useMemo(() => {
100103
let state: typeof cachedState.current = {
101-
status: immediateConnection ? "CONNECTING" : "READY",
104+
status: immediateConnection && url ? "CONNECTING" : "READY",
102105
data: null
103106
}
104107
return () => {
@@ -109,13 +112,11 @@ export const useEventSource = <T>({ url, opts, events, immediateConnection, onOp
109112
}
110113
return state;
111114
}
112-
}, [immediateConnection])
115+
}, [immediateConnection, url])
113116
);
114117

115118
const data = useMemo(() => (state.data), [state.data]);
116119

117-
useEffectOnce(() => () => close());
118-
119120
return {
120121
status: state.status,
121122
data,
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { useCallback, useMemo, useRef } from "react";
2+
import { TypedArray, UseWebSocketProps, UseWebSocketResult } from "../models"
3+
import { useSyncExternalStore } from ".";
4+
5+
/**
6+
* **`useWebSocket`**: Hook for creating and managing a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) connection to a server, as well as for sending and receiving data on the connection.
7+
* @param {UseWebSocketProps} param - object
8+
* @param {UseWebSocketProps} [param.url] - the URL to which to connect; this should be the URL to which the WebSocket server will respond.
9+
* @param {UseWebSocketProps} [param.protocols] - either a single protocol string or an array of protocol strings. These strings are used to indicate sub-protocols, so that a single server can implement multiple WebSocket sub-protocols.
10+
* @param {UseWebSocketProps} [param.binaryType] - the type of binary data being received over the WebSocket connection.
11+
* @param {UseWebSocketProps} [param.immediateConnection] - boolean to open webSocket connection immediatly.
12+
* @param {UseWebSocketProps} [param.onOpen] - function that will be executed when webSocket connection has been opened.
13+
* @param {UseWebSocketProps} [param.onMessage] - function that will be executed when message arrived from webSocket.
14+
* @param {UseWebSocketProps} [param.onError] - function that will be executed when an error occurred.
15+
* @param {UseWebSocketProps} [param.onClose] - function that will be executed when webSocket connection has been closed.
16+
* @param {UseWebSocketProps} [param.bufferingData] - boolean that indicates to use a buffer to keep data sent if connection aren't already opened.
17+
* @param {UseWebSocketProps} [param.autoReconnect] - boolean or object with properties __retries__, __delay__ and __onFailed__. If an error closes connection and its value isn't false or undefined, a connection will be restored every _delay_ milliseconds for __retries__ time: if connection won't be restored __onFailed__ function will be executed if it is present.
18+
* @returns {UseWebSocketResult} result
19+
* Object with these properties:
20+
* - __status__: string rapresenting webSocket state connection: __READY__ __CONNECTING__ __OPENED__ or __CLOSED__.
21+
* - __data__: last data value arrived from webSocket.
22+
* - __open__: function that opens connection with optional _url_ param .
23+
* - __send__: function that sends data by webSocket.
24+
* - __close__: function that closes connection with optional _code_ and _reason_ params.
25+
*/
26+
export const useWebSocket = <T = string | ArrayBuffer | Blob> ({ url, protocols, binaryType, onOpen, onMessage, onError, onClose, immediateConnection, bufferingData, autoReconnect }: UseWebSocketProps): UseWebSocketResult<T> => {
27+
const wsRef = useRef<WebSocket>();
28+
const alreadyOpened = useRef(false);
29+
const notifyRef = useRef<() => void>();
30+
const dataBuffer = useRef<(string | ArrayBuffer | Blob | TypedArray)[]>([]);
31+
const retryConnectId = useRef<ReturnType<typeof setTimeout>>();
32+
const urlRef = useRef<UseWebSocketProps["url"]>(url);
33+
const cachedState = useRef<{ status: UseWebSocketResult<T>["status"], data: UseWebSocketResult<T>["data"] }>({
34+
status: immediateConnection && url ? "CONNECTING" : "READY",
35+
data: null
36+
});
37+
const wsInitialized = !!wsRef.current;
38+
const currentStatus = cachedState.current.status;
39+
40+
const sendBuffer = useCallback(() => {
41+
if (bufferingData && wsInitialized && currentStatus === "OPENED") {
42+
dataBuffer.current.forEach(data => {
43+
wsRef.current!.send(data);
44+
});
45+
dataBuffer.current = [];
46+
}
47+
}, [bufferingData, currentStatus, wsInitialized])
48+
49+
if (url && immediateConnection && !alreadyOpened.current) {
50+
wsRef.current = new WebSocket(url, protocols);
51+
}
52+
53+
if (wsRef.current) {
54+
binaryType && (wsRef.current.binaryType = binaryType);
55+
wsRef.current.onopen = (evt: Event) => {
56+
!!retryConnectId.current && clearInterval(retryConnectId.current);
57+
cachedState.current.status = "OPENED";
58+
!!onOpen && onOpen(evt);
59+
notifyRef.current && notifyRef.current();
60+
sendBuffer();
61+
};
62+
wsRef.current.onclose = (evt: CloseEvent) => {
63+
cachedState.current.status = "CLOSED";
64+
!!retryConnectId.current && clearInterval(retryConnectId.current);
65+
!!onClose && onClose(evt);
66+
wsRef.current = undefined;
67+
notifyRef.current && notifyRef.current();
68+
};
69+
wsRef.current.onmessage = (evt: MessageEvent) => {
70+
cachedState.current.data = evt.data;
71+
!!onMessage && onMessage(evt);
72+
notifyRef.current && notifyRef.current();
73+
};
74+
wsRef.current.onerror = (evt: Event) => {
75+
cachedState.current.status = "CLOSED";
76+
!!onError && onError(evt);
77+
notifyRef.current && notifyRef.current();
78+
if (autoReconnect && !retryConnectId.current) {
79+
let retries: number, delay: number, onFailed: (() => void) | undefined;
80+
if (typeof autoReconnect === "boolean") {
81+
retries = 1;
82+
delay = 1000;
83+
} else {
84+
retries = autoReconnect.retries;
85+
delay = autoReconnect.delay;
86+
onFailed = autoReconnect.onFailed;
87+
}
88+
retryConnectId.current = setInterval(() => {
89+
if (retries === 0) {
90+
clearInterval(retryConnectId.current);
91+
retryConnectId.current = undefined;
92+
!!onFailed && onFailed();
93+
} else {
94+
wsRef.current = undefined;
95+
wsRef.current = new WebSocket(urlRef.current!, protocols);
96+
cachedState.current.status = "CONNECTING";
97+
notifyRef.current && notifyRef.current();
98+
}
99+
}, delay);
100+
}
101+
}
102+
}
103+
104+
const open = useCallback((urlParam?: UseWebSocketProps["url"]) => {
105+
if (!wsInitialized && (url || urlParam)) {
106+
const urlWs = url || urlParam as string | URL;
107+
wsRef.current = new WebSocket(urlWs, protocols);
108+
binaryType && (wsRef.current.binaryType = binaryType);
109+
cachedState.current.status = "CONNECTING";
110+
notifyRef.current && notifyRef.current();
111+
}
112+
}, [protocols, url, wsInitialized, binaryType]);
113+
114+
const send = useCallback((data: string | ArrayBuffer | Blob | TypedArray) => {
115+
if ((!wsInitialized || currentStatus !== "OPENED") && bufferingData) {
116+
dataBuffer.current.push(data);
117+
} else {
118+
sendBuffer();
119+
wsRef.current?.send(data);
120+
}
121+
}, [bufferingData, currentStatus, wsInitialized, sendBuffer]);
122+
123+
const close = useCallback((code?: number, reason?: string) => {
124+
if (wsInitialized && currentStatus === "OPENED") {
125+
wsRef.current?.close(code, reason);
126+
cachedState.current.status = "CLOSED";
127+
notifyRef.current && notifyRef.current();
128+
}
129+
}, [wsInitialized, currentStatus]);
130+
131+
const state = useSyncExternalStore(
132+
useCallback(notif => {
133+
notifyRef.current = notif;
134+
return () => {
135+
notifyRef.current = undefined;
136+
}
137+
}, []),
138+
useMemo(() => {
139+
let state: typeof cachedState.current = {
140+
status: immediateConnection && url ? "CONNECTING" : "READY",
141+
data: null
142+
}
143+
return () => {
144+
if (state.data !== cachedState.current.data || state.status !== cachedState.current.status) {
145+
state = {
146+
...cachedState.current
147+
}
148+
}
149+
return state;
150+
}
151+
}, [immediateConnection, url])
152+
);
153+
154+
const status = useMemo(() => state.status, [state.status]);
155+
const data = useMemo(() => state.data, [state.data]);
156+
157+
return {
158+
status,
159+
data,
160+
open,
161+
send,
162+
close
163+
}
164+
}

0 commit comments

Comments
 (0)