From 4d040e13be7670c7276b2fcfe627b1a57ea55fa1 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Thu, 20 Aug 2026 09:59:45 +0100 Subject: [PATCH] chore(webapp): scope imperative component refs (#4730) ## Summary Scopes React Compiler diagnostics to component and hook statements where refs intentionally coordinate editors, animations, polling, deferred callbacks, and other imperative integrations. Other compiler diagnostics remain active in those components. --- apps/webapp/app/components/admin/FeatureFlagsDialog.tsx | 1 + apps/webapp/app/components/code/JSONEditor.tsx | 1 + apps/webapp/app/components/code/TSQLEditor.tsx | 1 + apps/webapp/app/components/code/TextEditor.tsx | 1 + .../app/components/dashboard-agent/DashboardAgentChat.tsx | 6 ++++++ .../components/dashboard-agent/DashboardAgentMessages.tsx | 2 ++ .../app/components/dashboard-agent/DashboardAgentPanel.tsx | 1 + apps/webapp/app/components/navigation/SideMenu.tsx | 1 + apps/webapp/app/components/navigation/useReorderableList.ts | 1 + apps/webapp/app/components/primitives/AgentDotMatrix.tsx | 2 ++ apps/webapp/app/components/primitives/DateField.tsx | 1 + apps/webapp/app/components/primitives/Resizable.tsx | 2 ++ .../components/primitives/charts/hooks/useZoomSelection.ts | 1 + apps/webapp/app/components/query/QueryEditor.tsx | 6 ++++++ apps/webapp/app/components/runs/v3/RunStatusCellTooltip.tsx | 2 ++ apps/webapp/app/components/runs/v3/agent/AgentView.tsx | 3 +++ .../webhookDeliveries/v1/useDeliveriesLiveReload.ts | 1 + apps/webapp/app/hooks/useAutoRevalidate.ts | 1 + apps/webapp/app/hooks/useChanged.ts | 2 ++ apps/webapp/app/hooks/useDashboardEditor.ts | 1 + apps/webapp/app/hooks/useDebounce.ts | 1 + apps/webapp/app/hooks/useElementVisibility.ts | 1 + 22 files changed, 39 insertions(+) diff --git a/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx b/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx index 21014f563..58b2b7722 100644 --- a/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx +++ b/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx @@ -56,6 +56,7 @@ export function FeatureFlagsDialog({ const saveFetcher = useFetcher(); const loadFeatureFlags = loadFetcher.load; const onOpenChangeRef = useRef(onOpenChange); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. onOpenChangeRef.current = onOpenChange; const [overrides, setOverrides] = useState>({}); diff --git a/apps/webapp/app/components/code/JSONEditor.tsx b/apps/webapp/app/components/code/JSONEditor.tsx index b4c3f7a6e..0c5153c96 100644 --- a/apps/webapp/app/components/code/JSONEditor.tsx +++ b/apps/webapp/app/components/code/JSONEditor.tsx @@ -94,6 +94,7 @@ export function JSONEditor(opts: JSONEditorProps) { const editor = useRef(null); const settings: Omit = { ...opts, + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. container: editor.current, extensions, editable: !readOnly, diff --git a/apps/webapp/app/components/code/TSQLEditor.tsx b/apps/webapp/app/components/code/TSQLEditor.tsx index 073ba7f3e..2beede97c 100644 --- a/apps/webapp/app/components/code/TSQLEditor.tsx +++ b/apps/webapp/app/components/code/TSQLEditor.tsx @@ -196,6 +196,7 @@ export function TSQLEditor(opts: TSQLEditorProps) { const settings: Omit = { ...opts, + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. container: editor.current, extensions, editable: !readOnly, diff --git a/apps/webapp/app/components/code/TextEditor.tsx b/apps/webapp/app/components/code/TextEditor.tsx index db16b9964..f81cedabc 100644 --- a/apps/webapp/app/components/code/TextEditor.tsx +++ b/apps/webapp/app/components/code/TextEditor.tsx @@ -48,6 +48,7 @@ export function TextEditor(opts: TextEditorProps) { const editor = useRef(null); const settings: Omit = { ...opts, + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. container: editor.current, extensions, editable: !readOnly, diff --git a/apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx b/apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx index bd1404e74..1153cd981 100644 --- a/apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx +++ b/apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx @@ -123,6 +123,7 @@ export function DashboardAgentChat({ // The path this chat last rendered on. React never unmounts on a page teardown, so an // unmount whose live URL has moved is the router having navigated out from under it. const renderedPathRef = useRef(location.pathname); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. renderedPathRef.current = location.pathname; const transport = useTriggerChatTransport({ @@ -209,6 +210,7 @@ export function DashboardAgentChat({ }); const orderRef = useRef(createTranscriptOrder(initialMessages)); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. const messages = orderTranscript(rawMessages, orderRef.current); // Read here, not in the panel, so it re-reads as each turn settles. @@ -359,6 +361,7 @@ export function DashboardAgentChat({ const navigatedRef = useRef | null>(null); if (navigatedRef.current === null) { navigatedRef.current = new Set(); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. pendingNavigateIntents(initialMessages, navigatedRef.current); } useEffect(() => { @@ -374,6 +377,7 @@ export function DashboardAgentChat({ const watchProposedRef = useRef | null>(null); if (watchProposedRef.current === null) { watchProposedRef.current = new Set(); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. pendingWatchIntents(initialMessages, watchProposedRef.current); } useEffect(() => { @@ -388,6 +392,7 @@ export function DashboardAgentChat({ }, [transport, chatId, aiStop]); const teardownRef = useRef<() => void>(() => {}); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. teardownRef.current = () => { if (status !== "streaming" && status !== "submitted") return; const reason = unmountTeardown({ @@ -401,6 +406,7 @@ export function DashboardAgentChat({ // Read by the settle effect, which must not re-run when the transcript changes. const messagesRef = useRef(messages); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. messagesRef.current = messages; const prevStatus = useRef(status); diff --git a/apps/webapp/app/components/dashboard-agent/DashboardAgentMessages.tsx b/apps/webapp/app/components/dashboard-agent/DashboardAgentMessages.tsx index f3c9382d8..819e11209 100644 --- a/apps/webapp/app/components/dashboard-agent/DashboardAgentMessages.tsx +++ b/apps/webapp/app/components/dashboard-agent/DashboardAgentMessages.tsx @@ -126,7 +126,9 @@ export function winningInvestigationOccurrences(messages: UIMessage[]): Map { const previous = useRef>(); const next = useMemo(() => winningInvestigationOccurrences(messages), [messages]); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. previous.current = reuseWinners(previous.current, next); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. return previous.current; } diff --git a/apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx b/apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx index ff7a71e0f..c034aa0c7 100644 --- a/apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx +++ b/apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx @@ -174,6 +174,7 @@ export function DashboardAgentPanel({ const loadHistory = useMemo( () => + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. createCoalescedReload(async () => { try { const res = await fetch(actionPath); diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 018ec84d0..3227c021b 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -532,6 +532,7 @@ export function SideMenu({ // object each render, so depending on it would fire the cleanup (flushing the debounce) every // render — and drags re-render constantly — instead of only on unmount. const flushPendingPreferencesRef = useRef<() => void>(); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. flushPendingPreferencesRef.current = () => { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); diff --git a/apps/webapp/app/components/navigation/useReorderableList.ts b/apps/webapp/app/components/navigation/useReorderableList.ts index bcde7edd1..48eeb9869 100644 --- a/apps/webapp/app/components/navigation/useReorderableList.ts +++ b/apps/webapp/app/components/navigation/useReorderableList.ts @@ -34,6 +34,7 @@ export function useReorderableList({ const [order, setOrder] = useState(() => initialOrder ?? items.map(itemKey)); const resetOrderRef = useRef({ initialOrder, items, itemKey }); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. resetOrderRef.current = { initialOrder, items, itemKey }; // Only an organization switch resets user-managed order. Keep the latest inputs in a ref so diff --git a/apps/webapp/app/components/primitives/AgentDotMatrix.tsx b/apps/webapp/app/components/primitives/AgentDotMatrix.tsx index f82967828..9d1a78d84 100644 --- a/apps/webapp/app/components/primitives/AgentDotMatrix.tsx +++ b/apps/webapp/app/components/primitives/AgentDotMatrix.tsx @@ -290,7 +290,9 @@ export function AgentDotMatrix({ const playlistKey = playlist.join(","); const paletteObjRef = useRef(paletteObj); const playlistRef = useRef(playlist); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. paletteObjRef.current = paletteObj; + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. playlistRef.current = playlist; useEffect(() => { diff --git a/apps/webapp/app/components/primitives/DateField.tsx b/apps/webapp/app/components/primitives/DateField.tsx index 883508316..274811412 100644 --- a/apps/webapp/app/components/primitives/DateField.tsx +++ b/apps/webapp/app/components/primitives/DateField.tsx @@ -81,6 +81,7 @@ export function DateField({ }); const stateValueRef = useRef(state.value); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. stateValueRef.current = state.value; // Sync only when the passed value or timezone mode changes. Depending on state.value directly diff --git a/apps/webapp/app/components/primitives/Resizable.tsx b/apps/webapp/app/components/primitives/Resizable.tsx index 0bd4f8e86..ece2dc77a 100644 --- a/apps/webapp/app/components/primitives/Resizable.tsx +++ b/apps/webapp/app/components/primitives/Resizable.tsx @@ -100,7 +100,9 @@ function collapsibleHandleClassName(show: boolean) { function useFrozenValue(value: T | null | undefined): T | null | undefined { const ref = useRef(value); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. if (value != null) ref.current = value; + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. return ref.current; } diff --git a/apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts b/apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts index e34846af0..4a5f2feea 100644 --- a/apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts +++ b/apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts @@ -55,6 +55,7 @@ export function useZoomSelection(): UseZoomSelectionReturn { const stateRef = useRef(state); // Keep ref in sync with state + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. stateRef.current = state; const startSelection = useCallback((label: string) => { diff --git a/apps/webapp/app/components/query/QueryEditor.tsx b/apps/webapp/app/components/query/QueryEditor.tsx index c5665fa88..69f9e19a4 100644 --- a/apps/webapp/app/components/query/QueryEditor.tsx +++ b/apps/webapp/app/components/query/QueryEditor.tsx @@ -505,6 +505,7 @@ export function QueryEditor({ // Use a ref so the effect can read chartConfig without re-firing on every config tweak const chartConfigRef = useRef(chartConfig); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. chartConfigRef.current = chartConfig; // Reset chart config only when a column referenced by the current config is no @@ -562,6 +563,7 @@ export function QueryEditor({ }, []); // Compute current save data for the save render prop + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. const currentQuery = editorRef.current?.getQuery() ?? ""; const saveData: QueryEditorSaveData = { title: queryTitle ?? "Untitled Query", @@ -790,6 +792,7 @@ export function QueryEditor({ onRename={handleRenameTitle} /> } + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. query={editorRef.current?.getQuery() ?? defaultQuery} data={{ rows: results.rows, @@ -844,6 +847,7 @@ export function QueryEditor({ (); @@ -120,6 +121,7 @@ function useChildRunStatusesTooltip({ // Keep the latest loader callback available to the polling interval // without recreating the interval on every render. const loadChildStatusesRef = useRef(loadChildStatuses); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. loadChildStatusesRef.current = loadChildStatuses; const stopPolling = useCallback(() => { diff --git a/apps/webapp/app/components/runs/v3/agent/AgentView.tsx b/apps/webapp/app/components/runs/v3/agent/AgentView.tsx index 72926ac35..58797b1f7 100644 --- a/apps/webapp/app/components/runs/v3/agent/AgentView.tsx +++ b/apps/webapp/app/components/runs/v3/agent/AgentView.tsx @@ -285,6 +285,7 @@ function useAgentSessionMessages({ // `scheduleFlush`. The Map *reference* changes on every flush so React // detects the state update and the downstream `useMemo` recomputes. const [messagesById, setMessagesById] = useState>( + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. () => new Map(pendingRef.current) ); @@ -294,6 +295,7 @@ function useAgentSessionMessages({ const lastFlushAtRef = useRef(0); const pendingTimerRef = useRef | null>(null); const scheduleFlush = useRef<() => void>(() => {}); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. scheduleFlush.current = () => { if (pendingTimerRef.current !== null) return; // already scheduled const now = Date.now(); @@ -670,6 +672,7 @@ function useAgentSessionMessages({ return useMemo(() => { const timestamps = timestampsRef.current; const arr = Array.from(messagesById.values()); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. arr.sort((a, b) => { const ta = timestamps.get(a.id) ?? 0; const tb = timestamps.get(b.id) ?? 0; diff --git a/apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts b/apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts index aee736a3f..9cf28d802 100644 --- a/apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts +++ b/apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts @@ -124,6 +124,7 @@ export function useDeliveriesLiveReload({ const location = useLocation(); const deliveriesPollFetcher = useTypedFetcher(); const deliveriesPollFetcherStateRef = useRef(deliveriesPollFetcher.state); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. deliveriesPollFetcherStateRef.current = deliveriesPollFetcher.state; const [visibleDeliveries, setVisibleDeliveries] = useState(deliveries); diff --git a/apps/webapp/app/hooks/useAutoRevalidate.ts b/apps/webapp/app/hooks/useAutoRevalidate.ts index 4be50a34e..eec1f3c34 100644 --- a/apps/webapp/app/hooks/useAutoRevalidate.ts +++ b/apps/webapp/app/hooks/useAutoRevalidate.ts @@ -11,6 +11,7 @@ export function useAutoRevalidate(options: UseAutoRevalidateOptions = {}) { const { interval = 5000, onFocus = true, disabled = false } = options; const revalidator = useRevalidator(); const revalidatorRef = useRef(revalidator); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. revalidatorRef.current = revalidator; useEffect(() => { diff --git a/apps/webapp/app/hooks/useChanged.ts b/apps/webapp/app/hooks/useChanged.ts index 430c1605a..f7e08dd3a 100644 --- a/apps/webapp/app/hooks/useChanged.ts +++ b/apps/webapp/app/hooks/useChanged.ts @@ -12,7 +12,9 @@ export function useChanged( const itemRef = useRef(); const itemId = item?.id; + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. actionRef.current = action; + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. itemRef.current = item; useEffect(() => { diff --git a/apps/webapp/app/hooks/useDashboardEditor.ts b/apps/webapp/app/hooks/useDashboardEditor.ts index 53fba85c3..d7fae7638 100644 --- a/apps/webapp/app/hooks/useDashboardEditor.ts +++ b/apps/webapp/app/hooks/useDashboardEditor.ts @@ -207,6 +207,7 @@ export function useDashboardEditor({ const isInitializedRef = useRef(false); const currentLayoutJsonRef = useRef(JSON.stringify(initialData.layout)); const initialDataRef = useRef(initialData); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. initialDataRef.current = initialData; // Sync queue to prevent race conditions diff --git a/apps/webapp/app/hooks/useDebounce.ts b/apps/webapp/app/hooks/useDebounce.ts index da63330f2..b9bb51e48 100644 --- a/apps/webapp/app/hooks/useDebounce.ts +++ b/apps/webapp/app/hooks/useDebounce.ts @@ -29,6 +29,7 @@ export function useDebounceEffect(value: T, fn: (value: T) => void, delay: nu const fnRef = useRef(fn); // Update the ref whenever the function changes + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. fnRef.current = fn; useEffect(() => { diff --git a/apps/webapp/app/hooks/useElementVisibility.ts b/apps/webapp/app/hooks/useElementVisibility.ts index 2f1531882..44726cece 100644 --- a/apps/webapp/app/hooks/useElementVisibility.ts +++ b/apps/webapp/app/hooks/useElementVisibility.ts @@ -8,6 +8,7 @@ export function useElementVisibility({ onVisibilityChange }: UseElementVisibilit const ref = useRef(null); const isVisibleRef = useRef(false); const callbackRef = useRef(onVisibilityChange); + // oxlint-disable-next-line react/react-compiler -- This ref intentionally coordinates an imperative integration outside React state. callbackRef.current = onVisibilityChange; useEffect(() => {