UNPKG

@supunlakmal/hooks

Version:

A collection of reusable React hooks

170 lines 7.54 kB
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