@supunlakmal/hooks
Version:
A collection of reusable React hooks
170 lines • 7.54 kB
JavaScript
import { useState, useEffect, useRef, useCallback } from 'react';
export var ReadyState;
(function (ReadyState) {
ReadyState[ReadyState["Connecting"] = 0] = "Connecting";
ReadyState[ReadyState["Open"] = 1] = "Open";
ReadyState[ReadyState["Closing"] = 2] = "Closing";
ReadyState[ReadyState["Closed"] = 3] = "Closed";
})(ReadyState || (ReadyState = {}));
const DEFAULT_RECONNECT_LIMIT = 3;
const DEFAULT_RECONNECT_INTERVAL_MS = 5000;
/**
* Custom hook for managing WebSocket connections.
*
* @param {string | URL | null} url The URL to connect to. If null, the connection is not initiated automatically.
* @param {UseWebSocketOptions} [options={}] Hook options.
* @returns {UseWebSocketReturn} Object containing the WebSocket state and control functions.
*/
export const useWebSocket = (url, options = {}) => {
const { onOpen, onClose, onMessage, onError, reconnectLimit = DEFAULT_RECONNECT_LIMIT, reconnectIntervalMs = DEFAULT_RECONNECT_INTERVAL_MS, } = options;
const [readyState, setReadyState] = useState(ReadyState.Closed);
const [lastMessage, setLastMessage] = useState(null);
const [error, setError] = useState(null);
const wsRef = useRef(null);
const reconnectAttemptsRef = useRef(0);
const reconnectTimerRef = useRef(null);
const explicitDisconnectRef = useRef(false);
const savedOnOpen = useRef(onOpen);
const savedOnClose = useRef(onClose);
const savedOnMessage = useRef(onMessage);
const savedOnError = useRef(onError);
// Update saved callbacks when options change
useEffect(() => {
savedOnOpen.current = onOpen;
savedOnClose.current = onClose;
savedOnMessage.current = onMessage;
savedOnError.current = onError;
}, [onOpen, onClose, onMessage, onError]);
const connectWebSocket = useCallback(() => {
if (!url)
return; // Do nothing if URL is null
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
reconnectTimerRef.current = null;
}
if (wsRef.current && readyState !== ReadyState.Closed) {
console.warn('WebSocket already connected or connecting.');
return;
}
explicitDisconnectRef.current = false;
setReadyState(ReadyState.Connecting);
setError(null); // Reset error on new connection attempt
try {
const socket = new WebSocket(url);
wsRef.current = socket;
socket.onopen = (event) => {
var _a;
console.log('WebSocket opened');
reconnectAttemptsRef.current = 0; // Reset reconnect attempts on successful open
setReadyState(ReadyState.Open);
(_a = savedOnOpen.current) === null || _a === void 0 ? void 0 : _a.call(savedOnOpen, event);
};
socket.onmessage = (event) => {
var _a;
setLastMessage(event);
(_a = savedOnMessage.current) === null || _a === void 0 ? void 0 : _a.call(savedOnMessage, event);
};
socket.onerror = (event) => {
var _a;
console.error('WebSocket error:', event);
setError(event);
setReadyState(ReadyState.Closed); // Often errors lead to closure
(_a = savedOnError.current) === null || _a === void 0 ? void 0 : _a.call(savedOnError, event);
// Attempt reconnect on error
handleClose(false);
};
socket.onclose = (event) => {
var _a;
console.log('WebSocket closed:', event.code, event.reason);
// Only set Closed state here if not already handled by error
// and not explicitly disconnected
if (readyState !== ReadyState.Closed) {
setReadyState(ReadyState.Closed);
}
(_a = savedOnClose.current) === null || _a === void 0 ? void 0 : _a.call(savedOnClose, event);
wsRef.current = null;
// Handle reconnect logic
handleClose(explicitDisconnectRef.current);
};
}
catch (err) {
console.error('Failed to create WebSocket:', err);
// Ensure we pass an Event or null to setError
const errorEvent = err instanceof Event
? err
: new Event('error', {
// Create a generic event if err is not an Event
// You might want to add more details to this synthetic event if possible
});
setError(errorEvent);
setReadyState(ReadyState.Closed);
wsRef.current = null;
// Consider if reconnect should happen on constructor error
handleClose(true); // Treat constructor error as non-retryable for now
}
}, [url, readyState, reconnectLimit, reconnectIntervalMs]);
const handleClose = useCallback((wasExplicit) => {
if (wsRef.current && readyState !== ReadyState.Closed) {
setReadyState(ReadyState.Closed);
}
wsRef.current = null;
if (!wasExplicit && reconnectAttemptsRef.current < reconnectLimit) {
reconnectAttemptsRef.current++;
console.log(`WebSocket closed unexpectedly. Attempting reconnect ${reconnectAttemptsRef.current}/${reconnectLimit}...`);
reconnectTimerRef.current = setTimeout(() => {
connectWebSocket();
}, reconnectIntervalMs * Math.pow(2, reconnectAttemptsRef.current - 1)); // Exponential backoff
}
else if (!wasExplicit) {
console.log('WebSocket reconnect limit reached.');
}
}, [reconnectLimit, reconnectIntervalMs, connectWebSocket, readyState]);
const disconnectWebSocket = useCallback(() => {
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
reconnectTimerRef.current = null;
}
if (wsRef.current) {
console.log('Disconnecting WebSocket explicitly.');
explicitDisconnectRef.current = true;
setReadyState(ReadyState.Closing);
wsRef.current.close();
// onclose handler will set state to Closed and nullify wsRef
}
}, []);
// Initial connection effect
useEffect(() => {
if (url) {
connectWebSocket();
}
// Cleanup on unmount
return () => {
var _a;
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
}
explicitDisconnectRef.current = true; // Ensure no reconnect on unmount
(_a = wsRef.current) === null || _a === void 0 ? void 0 : _a.close();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]); // Rerun only if URL changes
const sendMessage = useCallback((data) => {
if (wsRef.current && wsRef.current.readyState === ReadyState.Open) {
wsRef.current.send(data);
}
else {
console.warn('WebSocket not open. Cannot send message.');
}
}, []);
const getWebSocket = useCallback(() => wsRef.current, []);
return {
sendMessage,
lastMessage,
readyState,
error,
connect: connectWebSocket,
disconnect: disconnectWebSocket,
getWebSocket,
};
};
//# sourceMappingURL=useWebSocket.js.map