fix(#338): Fix useWebSocket stale closure by using refs for callbacks
- Use useRef to store callbacks, preventing stale closures - Remove callback functions from useEffect dependencies - Only workspaceId and token trigger reconnects now - Callback changes update the ref without causing reconnects - Add 5 new tests verifying no reconnect on callback changes Refs #338 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { Socket } from "socket.io-client";
|
||||
import { io } from "socket.io-client";
|
||||
import { API_BASE_URL } from "@/lib/config";
|
||||
@@ -77,15 +77,14 @@ export function useWebSocket(
|
||||
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||
const [connectionError, setConnectionError] = useState<ConnectionError | null>(null);
|
||||
|
||||
const {
|
||||
onTaskCreated,
|
||||
onTaskUpdated,
|
||||
onTaskDeleted,
|
||||
onEventCreated,
|
||||
onEventUpdated,
|
||||
onEventDeleted,
|
||||
onProjectUpdated,
|
||||
} = callbacks;
|
||||
// Use refs for callbacks to prevent stale closures and unnecessary reconnects
|
||||
// This ensures that callback changes don't trigger useEffect re-runs
|
||||
const callbacksRef = useRef<WebSocketCallbacks>(callbacks);
|
||||
|
||||
// Keep the ref up-to-date with the latest callbacks
|
||||
useEffect(() => {
|
||||
callbacksRef.current = callbacks;
|
||||
}, [callbacks]);
|
||||
|
||||
useEffect(() => {
|
||||
// Use WebSocket URL from central config
|
||||
@@ -123,32 +122,48 @@ export function useWebSocket(
|
||||
setIsConnected(false);
|
||||
};
|
||||
|
||||
// Wrapper functions that always use the latest callbacks via ref
|
||||
// This prevents stale closure issues while avoiding reconnects on callback changes
|
||||
const handleTaskCreated = (task: Task): void => {
|
||||
callbacksRef.current.onTaskCreated?.(task);
|
||||
};
|
||||
|
||||
const handleTaskUpdated = (task: Task): void => {
|
||||
callbacksRef.current.onTaskUpdated?.(task);
|
||||
};
|
||||
|
||||
const handleTaskDeleted = (payload: DeletePayload): void => {
|
||||
callbacksRef.current.onTaskDeleted?.(payload);
|
||||
};
|
||||
|
||||
const handleEventCreated = (event: Event): void => {
|
||||
callbacksRef.current.onEventCreated?.(event);
|
||||
};
|
||||
|
||||
const handleEventUpdated = (event: Event): void => {
|
||||
callbacksRef.current.onEventUpdated?.(event);
|
||||
};
|
||||
|
||||
const handleEventDeleted = (payload: DeletePayload): void => {
|
||||
callbacksRef.current.onEventDeleted?.(payload);
|
||||
};
|
||||
|
||||
const handleProjectUpdated = (project: Project): void => {
|
||||
callbacksRef.current.onProjectUpdated?.(project);
|
||||
};
|
||||
|
||||
newSocket.on("connect", handleConnect);
|
||||
newSocket.on("disconnect", handleDisconnect);
|
||||
newSocket.on("connect_error", handleConnectError);
|
||||
|
||||
// Real-time event handlers
|
||||
if (onTaskCreated) {
|
||||
newSocket.on("task:created", onTaskCreated);
|
||||
}
|
||||
if (onTaskUpdated) {
|
||||
newSocket.on("task:updated", onTaskUpdated);
|
||||
}
|
||||
if (onTaskDeleted) {
|
||||
newSocket.on("task:deleted", onTaskDeleted);
|
||||
}
|
||||
if (onEventCreated) {
|
||||
newSocket.on("event:created", onEventCreated);
|
||||
}
|
||||
if (onEventUpdated) {
|
||||
newSocket.on("event:updated", onEventUpdated);
|
||||
}
|
||||
if (onEventDeleted) {
|
||||
newSocket.on("event:deleted", onEventDeleted);
|
||||
}
|
||||
if (onProjectUpdated) {
|
||||
newSocket.on("project:updated", onProjectUpdated);
|
||||
}
|
||||
// Register all event handlers - they'll check the ref for actual callbacks
|
||||
newSocket.on("task:created", handleTaskCreated);
|
||||
newSocket.on("task:updated", handleTaskUpdated);
|
||||
newSocket.on("task:deleted", handleTaskDeleted);
|
||||
newSocket.on("event:created", handleEventCreated);
|
||||
newSocket.on("event:updated", handleEventUpdated);
|
||||
newSocket.on("event:deleted", handleEventDeleted);
|
||||
newSocket.on("project:updated", handleProjectUpdated);
|
||||
|
||||
// Cleanup on unmount or dependency change
|
||||
return (): void => {
|
||||
@@ -156,27 +171,18 @@ export function useWebSocket(
|
||||
newSocket.off("disconnect", handleDisconnect);
|
||||
newSocket.off("connect_error", handleConnectError);
|
||||
|
||||
if (onTaskCreated) newSocket.off("task:created", onTaskCreated);
|
||||
if (onTaskUpdated) newSocket.off("task:updated", onTaskUpdated);
|
||||
if (onTaskDeleted) newSocket.off("task:deleted", onTaskDeleted);
|
||||
if (onEventCreated) newSocket.off("event:created", onEventCreated);
|
||||
if (onEventUpdated) newSocket.off("event:updated", onEventUpdated);
|
||||
if (onEventDeleted) newSocket.off("event:deleted", onEventDeleted);
|
||||
if (onProjectUpdated) newSocket.off("project:updated", onProjectUpdated);
|
||||
newSocket.off("task:created", handleTaskCreated);
|
||||
newSocket.off("task:updated", handleTaskUpdated);
|
||||
newSocket.off("task:deleted", handleTaskDeleted);
|
||||
newSocket.off("event:created", handleEventCreated);
|
||||
newSocket.off("event:updated", handleEventUpdated);
|
||||
newSocket.off("event:deleted", handleEventDeleted);
|
||||
newSocket.off("project:updated", handleProjectUpdated);
|
||||
|
||||
newSocket.disconnect();
|
||||
};
|
||||
}, [
|
||||
workspaceId,
|
||||
token,
|
||||
onTaskCreated,
|
||||
onTaskUpdated,
|
||||
onTaskDeleted,
|
||||
onEventCreated,
|
||||
onEventUpdated,
|
||||
onEventDeleted,
|
||||
onProjectUpdated,
|
||||
]);
|
||||
// Only stable values in deps - callbacks are accessed via ref
|
||||
}, [workspaceId, token]);
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
|
||||
Reference in New Issue
Block a user