refactor(site/src/pages/AgentsPage): clean up manual ref-sync callback patterns (#23732)

This commit is contained in:
Danielle Maywood
2026-03-27 23:09:51 +00:00
committed by GitHub
parent eda1bba969
commit 50d9d510c5
5 changed files with 28 additions and 40 deletions
+3 -6
View File
@@ -92,10 +92,7 @@ const AgentEmbedPage: FC = () => {
const embedSessionMutation = useMutation(
bootstrapChatEmbedSession({ checks: permissionChecks }, queryClient),
);
const latestEmbedSessionMutationRef = useRef(embedSessionMutation);
useEffect(() => {
latestEmbedSessionMutationRef.current = embedSessionMutation;
});
const inFlightBootstrapRef = useRef<Promise<unknown> | null>(null);
const [chatErrorReasons, setChatErrorReasons] = useState<
@@ -258,7 +255,7 @@ const AgentEmbedPage: FC = () => {
return;
}
const bootstrapPromise = latestEmbedSessionMutationRef.current
const bootstrapPromise = embedSessionMutation
.mutateAsync(token)
.catch(() => undefined)
.finally(() => {
@@ -277,7 +274,7 @@ const AgentEmbedPage: FC = () => {
return () => {
window.removeEventListener("message", handleMessage);
};
}, [agentId, isAwaitingBootstrapMessage]);
}, [agentId, isAwaitingBootstrapMessage, embedSessionMutation]);
const handleBootstrapRetry = () => {
inFlightBootstrapRef.current = null;
@@ -4,6 +4,7 @@ import { watchChat } from "#/api/api";
import { chatMessagesKey, updateInfiniteChatsCache } from "#/api/queries/chats";
import type * as TypesGen from "#/api/typesGenerated";
import { asNumber, asString } from "#/components/ai-elements/runtimeTypeUtils";
import { useEffectEvent } from "#/hooks/hookPolyfills";
import type { OneWayMessageEvent } from "#/utils/OneWayWebSocket";
import { createReconnectingWebSocket } from "#/utils/reconnectingWebSocket";
import {
@@ -686,12 +687,8 @@ export const useChatStore = (
// can call them without including them in its dependency array.
// This prevents the socket from tearing down when the parent
// re-renders with new callback identities.
const setChatErrorReasonRef = useRef(setChatErrorReason);
const clearChatErrorReasonRef = useRef(clearChatErrorReason);
useEffect(() => {
setChatErrorReasonRef.current = setChatErrorReason;
clearChatErrorReasonRef.current = clearChatErrorReason;
}, [setChatErrorReason, clearChatErrorReason]);
const setChatErrorReasonStable = useEffectEvent(setChatErrorReason);
const clearChatErrorReasonStable = useEffectEvent(clearChatErrorReason);
// True once the initial REST page has resolved for the current
// chat. The WebSocket effect gates on this so that
@@ -1009,7 +1006,7 @@ export const useChatStore = (
store.clearRetryState();
}
if (nextStatus !== "error") {
clearChatErrorReasonRef.current(chatID);
clearChatErrorReasonStable(chatID);
}
updateSidebarChat((chat) =>
chat.status === nextStatus
@@ -1026,7 +1023,7 @@ export const useChatStore = (
store.setChatStatus("error");
store.setStreamError(reason);
store.clearRetryState();
setChatErrorReasonRef.current(chatID, reason);
setChatErrorReasonStable(chatID, reason);
updateSidebarChat((chat) =>
chat.status === "error" ? chat : { ...chat, status: "error" },
);
@@ -1099,7 +1096,14 @@ export const useChatStore = (
}
activeChatIDRef.current = null;
};
}, [chatID, initialDataLoaded, queryClient, store]);
}, [
chatID,
initialDataLoaded,
queryClient,
store,
setChatErrorReasonStable,
clearChatErrorReasonStable,
]);
return {
store,
clearStreamError: () => {
@@ -610,7 +610,6 @@ const ScrollAnchoredContainer: FC<{
const observerRef = useRef<IntersectionObserver | null>(null);
const isFetchingRef = useRef(isFetchingMoreMessages);
const hasFetchedRef = useRef(false);
const onFetchRef = useRef(onFetchMoreMessages);
const autoScrollRef = useRef(true);
const contentRef = useRef<HTMLDivElement>(null);
const pendingPrependRef = useRef<{
@@ -629,8 +628,7 @@ const ScrollAnchoredContainer: FC<{
if (isFetchingMoreMessages) {
hasFetchedRef.current = true;
}
onFetchRef.current = onFetchMoreMessages;
}, [isFetchingMoreMessages, onFetchMoreMessages]);
}, [isFetchingMoreMessages]);
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
useEffect(() => {
@@ -672,7 +670,7 @@ const ScrollAnchoredContainer: FC<{
contentWidth: contentRect.width,
};
}
onFetchRef.current();
onFetchMoreMessages();
}
},
{
@@ -700,7 +698,7 @@ const ScrollAnchoredContainer: FC<{
observer.disconnect();
observerRef.current = null;
};
}, [scrollContainerRef]);
}, [scrollContainerRef, onFetchMoreMessages]);
// When a fetch completes, re-observe the sentinel to force
// the IntersectionObserver to re-evaluate. The observer only
@@ -84,6 +84,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { useEffectEvent } from "#/hooks/hookPolyfills";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { UserDropdownContent } from "#/modules/dashboard/Navbar/UserDropdown/UserDropdownContent";
import { useDashboard } from "#/modules/dashboard/useDashboard";
@@ -1401,14 +1402,9 @@ const LoadMoreSentinel: FC<{
isFetchingNextPage?: boolean;
}> = ({ onLoadMore, isFetchingNextPage }) => {
const sentinelRef = useRef<HTMLDivElement>(null);
const onLoadMoreRef = useRef(onLoadMore);
// Keep the callback ref in sync so the observer closure
// always calls the latest onLoadMore without needing to
// tear down and re-create the observer.
useEffect(() => {
onLoadMoreRef.current = onLoadMore;
}, [onLoadMore]);
const onLoadMoreStable = useEffectEvent(() => {
onLoadMore?.();
});
useEffect(() => {
// Don't observe while a fetch is in progress. When the
@@ -1424,15 +1420,15 @@ const LoadMoreSentinel: FC<{
const observer = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting && onLoadMoreRef.current) {
onLoadMoreRef.current();
if (entries[0]?.isIntersecting) {
onLoadMoreStable();
}
},
{ threshold: 0 },
);
observer.observe(el);
return () => observer.disconnect();
}, [isFetchingNextPage]);
}, [isFetchingNextPage, onLoadMoreStable]);
return (
<div ref={sentinelRef} className="flex items-center justify-center py-2">
@@ -122,13 +122,6 @@ export function useDesktopConnection({
);
},
});
// Stable ref so the effect can call the latest
// syncRemoteClipboardToLocal without listing it as a
// dependency (its identity may change across renders).
const syncClipboardRef = useRef(syncRemoteClipboardToLocal);
useEffect(() => {
syncClipboardRef.current = syncRemoteClipboardToLocal;
}, [syncRemoteClipboardToLocal]);
const attach = (container: HTMLElement) => {
const screen = offscreenContainerRef.current;
@@ -256,7 +249,7 @@ export function useDesktopConnection({
return;
}
setRemoteClipboardText(text);
syncClipboardRef.current(text).catch((err) => {
syncRemoteClipboardToLocal(text).catch((err) => {
console.error("Failed to sync remote clipboard to local:", err);
});
});
@@ -500,7 +493,7 @@ export function useDesktopConnection({
setStatus("idle");
setHasConnected(false);
};
}, [chatId]);
}, [chatId, syncRemoteClipboardToLocal]);
return {
status,