From 7673c46a02cecff29ea1635b89bda4d89b0c9f50 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Thu, 20 Aug 2026 09:59:43 +0100 Subject: [PATCH] chore(webapp): scope component effect synchronization (#4727) ## Summary Scopes React Compiler diagnostics for component and hook effects that intentionally synchronize with navigation, submissions, browser APIs, streams, timers, or authoritative server values. Each suppression stays on the reported synchronization call rather than disabling analysis for the component. --- apps/webapp/app/components/AskAI.tsx | 1 + apps/webapp/app/components/DevPresence.tsx | 2 ++ apps/webapp/app/components/Feedback.tsx | 1 + apps/webapp/app/components/LoginPageLayout.tsx | 1 + apps/webapp/app/components/TriggerRotatingLogo.tsx | 1 + .../app/components/admin/FeatureFlagsDialog.tsx | 3 +++ .../admin/backOffice/MaxProjectsSection.tsx | 2 ++ .../components/admin/backOffice/RateLimitSection.tsx | 2 ++ .../app/components/billing/BillingAlertsSection.tsx | 2 ++ .../components/billing/BillingLimitConfigSection.tsx | 1 + apps/webapp/app/components/code/AIQueryInput.tsx | 1 + .../app/components/dashboard-agent/AgentChart.tsx | 1 + .../dashboard-agent/DashboardAgentPanel.tsx | 5 +++++ .../integrations/VercelOnboardingModal.tsx | 12 ++++++++++++ apps/webapp/app/components/logs/LogDetailView.tsx | 2 ++ apps/webapp/app/components/logs/LogsTable.tsx | 1 + .../app/components/metrics/SaveToDashboardDialog.tsx | 1 + .../app/components/navigation/DashboardDialogs.tsx | 1 + .../components/navigation/EnvironmentSelector.tsx | 2 ++ .../app/components/navigation/FavoritesSection.tsx | 1 + apps/webapp/app/components/navigation/SideMenu.tsx | 6 ++++++ .../app/components/navigation/SideMenuHeader.tsx | 1 + .../app/components/primitives/AnimatedCallout.tsx | 2 ++ apps/webapp/app/components/primitives/Buttons.tsx | 1 + apps/webapp/app/components/primitives/Checkbox.tsx | 1 + .../app/components/primitives/ClipboardField.tsx | 1 + .../app/components/primitives/DurationPicker.tsx | 1 + .../app/components/primitives/MiddleTruncate.tsx | 1 + .../webapp/app/components/primitives/SearchInput.tsx | 1 + .../app/components/primitives/TooltipPortal.tsx | 1 + apps/webapp/app/components/query/QueryEditor.tsx | 1 + apps/webapp/app/components/queues/QueueControls.tsx | 1 + apps/webapp/app/components/runs/v3/AIFilterInput.tsx | 2 ++ .../app/components/runs/v3/RunStatusCellTooltip.tsx | 2 ++ apps/webapp/app/components/runs/v3/SharedFilters.tsx | 1 + .../app/components/runs/v3/ai/AIChatMessages.tsx | 1 + .../components/schedules/PurchaseSchedulesModal.tsx | 1 + .../components/webhookConsole/SampleSourcePicker.tsx | 1 + .../webhookDeliveries/v1/useDeliveriesLiveReload.ts | 2 ++ apps/webapp/app/hooks/useEventSource.tsx | 1 + apps/webapp/app/hooks/useMetricResourceQuery.ts | 1 + 41 files changed, 73 insertions(+) diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx index 37911b873..2f31a9704 100644 --- a/apps/webapp/app/components/AskAI.tsx +++ b/apps/webapp/app/components/AskAI.tsx @@ -273,6 +273,7 @@ function ChatMessages({ // Reset feedback state when conversation is reset useEffect(() => { if (conversation.length === 0) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setFeedbackGivenForQAs(new Set()); } }, [conversation.length]); diff --git a/apps/webapp/app/components/DevPresence.tsx b/apps/webapp/app/components/DevPresence.tsx index 907a52072..cd9b79cf1 100644 --- a/apps/webapp/app/components/DevPresence.tsx +++ b/apps/webapp/app/components/DevPresence.tsx @@ -55,6 +55,7 @@ export function DevPresenceProvider({ children, enabled = true }: DevPresencePro useEffect(() => { // If disabled or no events if (!enabled || streamedEvents === null) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsConnected(undefined); return; } @@ -113,6 +114,7 @@ export function useCrossEngineIsConnected({ useEffect(() => { if (project.engine === "V2") { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setCrossEngineIsConnected(isConnected); return; } diff --git a/apps/webapp/app/components/Feedback.tsx b/apps/webapp/app/components/Feedback.tsx index f6a9971cc..52d7bb950 100644 --- a/apps/webapp/app/components/Feedback.tsx +++ b/apps/webapp/app/components/Feedback.tsx @@ -76,6 +76,7 @@ export function Feedback({ useEffect(() => { const open = searchParams.get("feedbackPanel"); if (open) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setType(open as FeedbackType); setOpen(true); // Clone instead of mutating in place diff --git a/apps/webapp/app/components/LoginPageLayout.tsx b/apps/webapp/app/components/LoginPageLayout.tsx index 1db614eb9..a0c2cbf52 100644 --- a/apps/webapp/app/components/LoginPageLayout.tsx +++ b/apps/webapp/app/components/LoginPageLayout.tsx @@ -47,6 +47,7 @@ export function LoginPageLayout({ const [randomQuote, setRandomQuote] = useState(null); useEffect(() => { const randomIndex = Math.floor(Math.random() * quotes.length); + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setRandomQuote(quotes[randomIndex]); }, []); diff --git a/apps/webapp/app/components/TriggerRotatingLogo.tsx b/apps/webapp/app/components/TriggerRotatingLogo.tsx index 878c203a3..055fd7703 100644 --- a/apps/webapp/app/components/TriggerRotatingLogo.tsx +++ b/apps/webapp/app/components/TriggerRotatingLogo.tsx @@ -25,6 +25,7 @@ export function TriggerRotatingLogo() { useEffect(() => { // Already registered from a previous render if (customElements.get("spline-viewer")) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsSplineReady(true); return; } diff --git a/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx b/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx index 8433c74f3..21014f563 100644 --- a/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx +++ b/apps/webapp/app/components/admin/FeatureFlagsDialog.tsx @@ -67,6 +67,7 @@ export function FeatureFlagsDialog({ useEffect(() => { if (open && orgId) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setSaveError(null); setOverrides({}); setInitialOverrides({}); @@ -77,6 +78,7 @@ export function FeatureFlagsDialog({ useEffect(() => { if (loadFetcher.data) { const loaded = loadFetcher.data.orgFlags ?? {}; + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setOverrides({ ...loaded }); setInitialOverrides({ ...loaded }); } @@ -86,6 +88,7 @@ export function FeatureFlagsDialog({ if (saveFetcher.data?.success) { onOpenChangeRef.current(false); } else if (saveFetcher.data?.error) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setSaveError(saveFetcher.data.error); } }, [saveFetcher.data]); diff --git a/apps/webapp/app/components/admin/backOffice/MaxProjectsSection.tsx b/apps/webapp/app/components/admin/backOffice/MaxProjectsSection.tsx index 6b0185e33..7375cdb6c 100644 --- a/apps/webapp/app/components/admin/backOffice/MaxProjectsSection.tsx +++ b/apps/webapp/app/components/admin/backOffice/MaxProjectsSection.tsx @@ -34,10 +34,12 @@ export function MaxProjectsSection({ const [value, setValue] = useState(String(maximumProjectCount)); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. if (hasFieldErrors) setIsEditing(true); }, [hasFieldErrors]); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. if (savedJustNow && !hasFieldErrors) setIsEditing(false); }, [savedJustNow, hasFieldErrors]); diff --git a/apps/webapp/app/components/admin/backOffice/RateLimitSection.tsx b/apps/webapp/app/components/admin/backOffice/RateLimitSection.tsx index 5da447ff1..b9f2c5bff 100644 --- a/apps/webapp/app/components/admin/backOffice/RateLimitSection.tsx +++ b/apps/webapp/app/components/admin/backOffice/RateLimitSection.tsx @@ -65,10 +65,12 @@ export function RateLimitSection({ const [maxTokens, setMaxTokens] = useState(current ? String(current.maxTokens) : ""); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. if (hasFieldErrors) setIsEditing(true); }, [hasFieldErrors]); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. if (savedJustNow && !hasFieldErrors) setIsEditing(false); }, [savedJustNow, hasFieldErrors]); diff --git a/apps/webapp/app/components/billing/BillingAlertsSection.tsx b/apps/webapp/app/components/billing/BillingAlertsSection.tsx index ed95af592..b3afe574f 100644 --- a/apps/webapp/app/components/billing/BillingAlertsSection.tsx +++ b/apps/webapp/app/components/billing/BillingAlertsSection.tsx @@ -119,6 +119,7 @@ export function BillingAlertsSection({ return; } + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setShowResetBanner(true); if (searchParams.get("alertsReset") !== "1") { @@ -189,6 +190,7 @@ export function BillingAlertsSection({ useEffect(() => { nextThresholdIdRef.current = savedThresholds.length; + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setThresholdRows(toThresholdRows(savedThresholds)); setEmailValues(savedEmails.length > 0 ? [...savedEmails, ""] : [""]); }, [savedThresholds, savedEmails]); diff --git a/apps/webapp/app/components/billing/BillingLimitConfigSection.tsx b/apps/webapp/app/components/billing/BillingLimitConfigSection.tsx index e6362d4cb..d031bb6c6 100644 --- a/apps/webapp/app/components/billing/BillingLimitConfigSection.tsx +++ b/apps/webapp/app/components/billing/BillingLimitConfigSection.tsx @@ -126,6 +126,7 @@ export function BillingLimitConfigSection({ const formRef = useRef(null); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setMode(resetMode); setCustomAmount(savedCustomAmount); setCancelInProgressRuns(savedCancelInProgressRuns); diff --git a/apps/webapp/app/components/code/AIQueryInput.tsx b/apps/webapp/app/components/code/AIQueryInput.tsx index e06d03268..9ac49fba7 100644 --- a/apps/webapp/app/components/code/AIQueryInput.tsx +++ b/apps/webapp/app/components/code/AIQueryInput.tsx @@ -61,6 +61,7 @@ export function AIQueryInput({ // If mode is edit but there's no current query, switch to new useEffect(() => { if (mode === "edit" && !canEdit) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setMode("new"); } }, [mode, canEdit]); diff --git a/apps/webapp/app/components/dashboard-agent/AgentChart.tsx b/apps/webapp/app/components/dashboard-agent/AgentChart.tsx index dda01b7f1..70beed5a2 100644 --- a/apps/webapp/app/components/dashboard-agent/AgentChart.tsx +++ b/apps/webapp/app/components/dashboard-agent/AgentChart.tsx @@ -93,6 +93,7 @@ export function AgentChart({ // The block can render before `query` has streamed in; an empty query 400s. if (!block.query) return; if (!organizationId || !projectId || !environmentId) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setState({ status: "error", error: "No environment context to run the query." }); return; } diff --git a/apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx b/apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx index 598167f26..ff7a71e0f 100644 --- a/apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx +++ b/apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx @@ -168,6 +168,7 @@ export function DashboardAgentPanel({ // Ordering-safe: if the new chat has not reported yet, its own report re-sets the marker. useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setThinkingChatId((previous) => markerAfterActiveChat(previous, active?.chatId)); }, [active?.chatId]); @@ -313,6 +314,7 @@ export function DashboardAgentPanel({ void loadHistory(); const stored = readLastChat(storageKey); if (stored && stored.path === location.pathname) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. void openChat(stored.chatId); } else { setLoading(false); @@ -341,6 +343,7 @@ export function DashboardAgentPanel({ handledOpenChatSeq.current = openChatRequest.seq; // Reloading the visible transcript would drop a turn in flight. if (openChatRequest.chatId === active?.chatId) return; + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. void openChat(openChatRequest.chatId); // `active` is read, not tracked: a later change must not re-run the request. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -357,6 +360,7 @@ export function DashboardAgentPanel({ onChatRead?.(chatId, { leaving: false }); visibleChatId.current = nextVisibleChat(chatId, { leaving: false }); justRead.current.add(chatId); + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setChats((previous) => markChatListRead(previous, chatId)); // Read again on the way out: a wake can land while the chat is open. return () => { @@ -384,6 +388,7 @@ export function DashboardAgentPanel({ if (target === "hold") return; handledRequestSeq.current = requestedMessage.seq; if (target === "new-chat") { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. void createChat(requestedMessage.text); return; } diff --git a/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx b/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx index d606b5125..189f409dc 100644 --- a/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx +++ b/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx @@ -183,6 +183,7 @@ export function VercelOnboardingModal({ hasSyncedStagingRef.current = false; hasSyncedPreviewRef.current = false; } else if (isOpen && state === "idle") { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setState(computeInitialState()); } prevIsOpenRef.current = isOpen; @@ -262,6 +263,7 @@ export function VercelOnboardingModal({ // Strip "stg" from build settings when the staging environment mapping is cleared useEffect(() => { if (!vercelStagingEnvironment) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setPullEnvVarsBeforeBuild((prev) => prev.filter((s) => s !== "stg")); setDiscoverEnvVars((prev) => prev.filter((s) => s !== "stg")); } @@ -329,6 +331,7 @@ export function VercelOnboardingModal({ useEffect(() => { if (!isOpen) { hasTriggeredMarketplaceRedirectRef.current = false; + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsRedirecting(false); } }, [isOpen]); @@ -390,6 +393,7 @@ export function VercelOnboardingModal({ state === "loading-projects" && onboardingData?.availableProjects !== undefined ) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setState("project-selection"); } }, [state, onboardingData?.availableProjects, onboardingData?.authInvalid]); @@ -400,6 +404,7 @@ export function VercelOnboardingModal({ state === "loading-env-vars" && onboardingData?.environmentVariables ) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setState("env-var-sync"); } }, [state, onboardingData?.environmentVariables, onboardingData?.authInvalid]); @@ -415,6 +420,7 @@ export function VercelOnboardingModal({ trackOnboarding("vercel onboarding project selected", { vercel_project_name: selectedVercelProject?.name, }); + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setState("loading-env-mapping"); if (onDataReload) { onDataReload(); @@ -437,6 +443,7 @@ export function VercelOnboardingModal({ const hasCustomEnvs = (onboardingData.customEnvironments?.length ?? 0) > 0 && hasStagingEnvironment; if (hasCustomEnvs && !fromMarketplaceContext) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setState("env-mapping"); } else { setState("loading-env-vars"); @@ -661,6 +668,7 @@ export function VercelOnboardingModal({ } return; } + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setState("completed"); } }, [completeOnboardingFetcher.data, completeOnboardingFetcher.state, state]); @@ -675,6 +683,7 @@ export function VercelOnboardingModal({ return; } } + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setState("completed"); } }, [state, isGitHubConnectedForOnboarding, fromMarketplaceContext, nextUrl, trackOnboarding]); @@ -704,6 +713,7 @@ export function VercelOnboardingModal({ envMappingFetcher.data.success && envMappingFetcher.state === "idle" ) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setState("loading-env-vars"); } }, [envMappingFetcher.data, envMappingFetcher.state]); @@ -719,12 +729,14 @@ export function VercelOnboardingModal({ selectedEnv = stagingEnv ?? customEnvironments[0]; } + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setVercelStagingEnvironment({ environmentId: selectedEnv.id, displayName: selectedEnv.slug }); } }, [state, customEnvironments, vercelStagingEnvironment]); useEffect(() => { if (state === "project-selection" && availableProjects.length > 0 && !selectedVercelProject) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setSelectedVercelProject(availableProjects[0]); } }, [state, availableProjects, selectedVercelProject]); diff --git a/apps/webapp/app/components/logs/LogDetailView.tsx b/apps/webapp/app/components/logs/LogDetailView.tsx index c0f52b76a..adf691884 100644 --- a/apps/webapp/app/components/logs/LogDetailView.tsx +++ b/apps/webapp/app/components/logs/LogDetailView.tsx @@ -68,6 +68,7 @@ export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDet useEffect(() => { if (!logId) return; + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setError(null); fetcher.load( `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${ @@ -80,6 +81,7 @@ export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDet // Handle fetch errors useEffect(() => { if (fetcher.data && typeof fetcher.data === "object" && "error" in fetcher.data) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setError(fetcher.data.error as string); } else if (fetcher.state === "idle" && fetcher.data === null && !initialLog) { setError("Failed to load log details"); diff --git a/apps/webapp/app/components/logs/LogsTable.tsx b/apps/webapp/app/components/logs/LogsTable.tsx index 32016a6a7..73cefe32a 100644 --- a/apps/webapp/app/components/logs/LogsTable.tsx +++ b/apps/webapp/app/components/logs/LogsTable.tsx @@ -76,6 +76,7 @@ export function LogsTable({ // Show load more spinner only after 0.2 seconds of loading time useEffect(() => { if (!isLoadingMore) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setShowLoadMoreSpinner(false); return; } diff --git a/apps/webapp/app/components/metrics/SaveToDashboardDialog.tsx b/apps/webapp/app/components/metrics/SaveToDashboardDialog.tsx index 19f50436d..90ebec0e9 100644 --- a/apps/webapp/app/components/metrics/SaveToDashboardDialog.tsx +++ b/apps/webapp/app/components/metrics/SaveToDashboardDialog.tsx @@ -88,6 +88,7 @@ export function SaveToDashboardDialog({ useEffect(() => { if (customDashboards.length > 0 && !selectedDashboardId) { const available = customDashboards.find((d) => d.widgetCount < widgetLimit); + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setSelectedDashboardId(available?.friendlyId ?? customDashboards[0].friendlyId); } }, [customDashboards, selectedDashboardId, widgetLimit]); diff --git a/apps/webapp/app/components/navigation/DashboardDialogs.tsx b/apps/webapp/app/components/navigation/DashboardDialogs.tsx index 85fcf4c42..f9e4c643b 100644 --- a/apps/webapp/app/components/navigation/DashboardDialogs.tsx +++ b/apps/webapp/app/components/navigation/DashboardDialogs.tsx @@ -52,6 +52,7 @@ function useCreateDashboard({ useEffect(() => { if (navigation.formAction === formAction && navigation.state === "loading") { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsOpen(false); } }, [navigation.formAction, navigation.state, formAction]); diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index 99cfdaa54..925c583d5 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -61,6 +61,7 @@ export function EnvironmentSelector({ const revalidator = useRevalidator(); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsMenuOpen(false); }, [navigation.location?.pathname]); @@ -249,6 +250,7 @@ function Branches({ }, []); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setMenuOpen(false); }, [navigation.location?.pathname]); diff --git a/apps/webapp/app/components/navigation/FavoritesSection.tsx b/apps/webapp/app/components/navigation/FavoritesSection.tsx index 835289243..48c6ecf1f 100644 --- a/apps/webapp/app/components/navigation/FavoritesSection.tsx +++ b/apps/webapp/app/components/navigation/FavoritesSection.tsx @@ -49,6 +49,7 @@ export function FavoriteMenuItem({ // Watch search too: navigating to a favorite can change only the search on the same pathname useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setMenuOpen(false); }, [navigation.location?.pathname, navigation.location?.search]); diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 640d0c6eb..d653d44dc 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -438,6 +438,7 @@ export function SideMenu({ const data = customizationFetcher.data; if (!data) { // Settled with no response body (e.g. a session-expiry redirect): fail rather than spin + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setCustomizeConfirmPending(false); setCustomizeError("Couldn't save your changes. Please try again."); return; @@ -1488,6 +1489,7 @@ function SideMenuMoreItem({ // Watch search too: navigating to a favorite can change only the search on the same pathname useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setOpen(false); }, [navigation.location?.pathname, navigation.location?.search]); @@ -1701,6 +1703,7 @@ function OrgSelector({ const planTitle = currentPlan?.v3Subscription?.plan?.title; useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setOrgMenuOpen(false); }, [navigation.location?.pathname]); @@ -1979,6 +1982,7 @@ function AccountMenu({ isAdmin, isImpersonating }: { isAdmin: boolean; isImperso const navigation = useNavigation(); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsOpen(false); }, [navigation.location?.pathname]); @@ -2033,6 +2037,7 @@ function ProjectSelector({ const { urlForProject } = usePageSwitcher(); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsMenuOpen(false); }, [navigation.location?.pathname]); @@ -2154,6 +2159,7 @@ function SideMenuPopoverSubMenu({ // Close the submenu on navigation (the parent popover closes too). useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsOpen(false); }, [navigation.location?.pathname]); diff --git a/apps/webapp/app/components/navigation/SideMenuHeader.tsx b/apps/webapp/app/components/navigation/SideMenuHeader.tsx index 698ab1a23..4bd88c2f6 100644 --- a/apps/webapp/app/components/navigation/SideMenuHeader.tsx +++ b/apps/webapp/app/components/navigation/SideMenuHeader.tsx @@ -19,6 +19,7 @@ export function SideMenuHeader({ const navigation = useNavigation(); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setHeaderMenuOpen(false); }, [navigation.location?.pathname]); diff --git a/apps/webapp/app/components/primitives/AnimatedCallout.tsx b/apps/webapp/app/components/primitives/AnimatedCallout.tsx index 2391f893e..e71ba8bd7 100644 --- a/apps/webapp/app/components/primitives/AnimatedCallout.tsx +++ b/apps/webapp/app/components/primitives/AnimatedCallout.tsx @@ -41,12 +41,14 @@ export function AnimatedCallout({ useEffect(() => { if (!show) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setAutoDismissed(false); } }, [show]); useEffect(() => { if (shouldShow) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setRendered(true); return; } diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index d6ec92f79..c43326060 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -261,6 +261,7 @@ export function ButtonContent(props: ButtonContentPropsType) { const [showSpinner, setShowSpinner] = useState(false); useEffect(() => { if (!isLoading) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setShowSpinner(false); return; } diff --git a/apps/webapp/app/components/primitives/Checkbox.tsx b/apps/webapp/app/components/primitives/Checkbox.tsx index e22be7e71..6972294c4 100644 --- a/apps/webapp/app/components/primitives/Checkbox.tsx +++ b/apps/webapp/app/components/primitives/Checkbox.tsx @@ -111,6 +111,7 @@ export const CheckboxWithLabel = React.forwardRef { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsChecked(defaultChecked ?? false); }, [defaultChecked]); diff --git a/apps/webapp/app/components/primitives/ClipboardField.tsx b/apps/webapp/app/components/primitives/ClipboardField.tsx index 73facf3bc..c3498a915 100644 --- a/apps/webapp/app/components/primitives/ClipboardField.tsx +++ b/apps/webapp/app/components/primitives/ClipboardField.tsx @@ -120,6 +120,7 @@ export function ClipboardField({ const { container, input, buttonVariant, button, size } = variants[variant]; useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsSecure(secure !== undefined && secure); }, [secure]); diff --git a/apps/webapp/app/components/primitives/DurationPicker.tsx b/apps/webapp/app/components/primitives/DurationPicker.tsx index e1ce40f5d..991563cec 100644 --- a/apps/webapp/app/components/primitives/DurationPicker.tsx +++ b/apps/webapp/app/components/primitives/DurationPicker.tsx @@ -48,6 +48,7 @@ export function DurationPicker({ const newMinutes = Math.floor((controlledValue % 3600) / 60); const newSeconds = controlledValue % 60; + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setHours(newHours); setMinutes(newMinutes); setSeconds(newSeconds); diff --git a/apps/webapp/app/components/primitives/MiddleTruncate.tsx b/apps/webapp/app/components/primitives/MiddleTruncate.tsx index 45915c252..2cf6f3d0e 100644 --- a/apps/webapp/app/components/primitives/MiddleTruncate.tsx +++ b/apps/webapp/app/components/primitives/MiddleTruncate.tsx @@ -117,6 +117,7 @@ export function MiddleTruncate({ text, className }: MiddleTruncateProps) { }, [text]); useLayoutEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. calculateTruncation(); // Recalculate on resize (guard for jsdom/older browsers) diff --git a/apps/webapp/app/components/primitives/SearchInput.tsx b/apps/webapp/app/components/primitives/SearchInput.tsx index 0ec8a4d64..2597cb9ec 100644 --- a/apps/webapp/app/components/primitives/SearchInput.tsx +++ b/apps/webapp/app/components/primitives/SearchInput.tsx @@ -70,6 +70,7 @@ export function SearchInput({ // Only mark synced once we actually apply it, so a URL change during focus still syncs on blur. if (!isFocused) { lastSyncedRef.current = urlSearch; + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setText(urlSearch); } }, [isControlled, controlledValue, value, isFocused, paramName]); diff --git a/apps/webapp/app/components/primitives/TooltipPortal.tsx b/apps/webapp/app/components/primitives/TooltipPortal.tsx index 011c5aae7..42fd4b282 100644 --- a/apps/webapp/app/components/primitives/TooltipPortal.tsx +++ b/apps/webapp/app/components/primitives/TooltipPortal.tsx @@ -44,6 +44,7 @@ export default function TooltipPortal({ active = true, children }: PopperPortalP useEffect(() => { const el = document.createElement("div"); document.body.appendChild(el); + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setPortalElement(el); return () => el.remove(); }, []); diff --git a/apps/webapp/app/components/query/QueryEditor.tsx b/apps/webapp/app/components/query/QueryEditor.tsx index 8c842c35f..c5665fa88 100644 --- a/apps/webapp/app/components/query/QueryEditor.tsx +++ b/apps/webapp/app/components/query/QueryEditor.tsx @@ -985,6 +985,7 @@ function QueryTitle({ // Update rename value when title changes useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setRenameValue(title ?? ""); }, [title]); diff --git a/apps/webapp/app/components/queues/QueueControls.tsx b/apps/webapp/app/components/queues/QueueControls.tsx index 7817749b0..5cfe0c69e 100644 --- a/apps/webapp/app/components/queues/QueueControls.tsx +++ b/apps/webapp/app/components/queues/QueueControls.tsx @@ -183,6 +183,7 @@ export function QueueOverrideConcurrencyButton({ useEffect(() => { if (navigation.state === "loading" || navigation.state === "idle") { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsOpen(false); } }, [navigation.state]); diff --git a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx index 10b4986c2..5faf15814 100644 --- a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx +++ b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx @@ -39,6 +39,7 @@ export function AIFilterInput() { useEffect(() => { if (fetcher.data?.success && fetcher.state === "loading") { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setText(""); setIsFocused(false); @@ -184,6 +185,7 @@ function ErrorPopover({ useEffect(() => { if (error) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setIsOpen(true); } if (timeout.current) { diff --git a/apps/webapp/app/components/runs/v3/RunStatusCellTooltip.tsx b/apps/webapp/app/components/runs/v3/RunStatusCellTooltip.tsx index 789572d7d..ea65bfaab 100644 --- a/apps/webapp/app/components/runs/v3/RunStatusCellTooltip.tsx +++ b/apps/webapp/app/components/runs/v3/RunStatusCellTooltip.tsx @@ -144,6 +144,7 @@ function useChildRunStatusesTooltip({ const entry = fetcher.data.runs.find((run) => run.friendlyId === friendlyId); if (!entry) return; + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setChildStatuses((previous) => areChildStatusesEqual(previous, entry.statuses) ? previous : entry.statuses ); @@ -169,6 +170,7 @@ function useChildRunStatusesTooltip({ useEffect(() => { prevHasFinishedRef.current = hasFinished; stopPolling(); + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setChildStatuses(undefined); if (isOpenRef.current) { loadChildStatuses(); diff --git a/apps/webapp/app/components/runs/v3/SharedFilters.tsx b/apps/webapp/app/components/runs/v3/SharedFilters.tsx index 70832a1d0..0588ece06 100644 --- a/apps/webapp/app/components/runs/v3/SharedFilters.tsx +++ b/apps/webapp/app/components/runs/v3/SharedFilters.tsx @@ -519,6 +519,7 @@ function TimeDropdown({ // Sync state when props change useEffect(() => { const parsed = getInitialCustomDuration(period); + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setCustomValue(parsed.value); setCustomUnit(parsed.unit); diff --git a/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx b/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx index 96b1b094e..bdafce941 100644 --- a/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx +++ b/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx @@ -289,6 +289,7 @@ export function ToolUseRow({ tool }: { tool: ToolUse }) { // Auto-select input tab when input arrives after initial render (e.g. streaming tool calls) useEffect(() => { if (!hasSubAgent && hasInput) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setActiveTab((current) => current ?? "input"); } }, [hasInput, hasSubAgent]); diff --git a/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx b/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx index 67a142ce3..497486b53 100644 --- a/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx +++ b/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx @@ -88,6 +88,7 @@ export function PurchaseSchedulesModal({ "ok" in data && data.ok ) { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setOpen(false); } }, [fetcher.state, fetcher.data]); diff --git a/apps/webapp/app/components/webhookConsole/SampleSourcePicker.tsx b/apps/webapp/app/components/webhookConsole/SampleSourcePicker.tsx index 272cda08a..7f2c2b83b 100644 --- a/apps/webapp/app/components/webhookConsole/SampleSourcePicker.tsx +++ b/apps/webapp/app/components/webhookConsole/SampleSourcePicker.tsx @@ -46,6 +46,7 @@ export function SampleSourcePicker({ useEffect(() => { if (!providers || providers.length === 0) return; + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setSelectedProvider((current) => { if (current && providers.some((p) => p.id === current)) return current; if (endpointSource && providers.some((p) => p.id === endpointSource)) return endpointSource; diff --git a/apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts b/apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts index 3905a8aea..aee736a3f 100644 --- a/apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts +++ b/apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts @@ -140,6 +140,7 @@ export function useDeliveriesLiveReload({ } = useNewDeliveriesDetection({ deliveries, isLoading }); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setVisibleDeliveries(deliveries); resetNewDeliveriesTracking(); }, [deliveries, location.search, resetNewDeliveriesTracking]); @@ -148,6 +149,7 @@ export function useDeliveriesLiveReload({ const data = deliveriesPollFetcher.data; if (!data?.deliveries.length) return; + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setVisibleDeliveries((current) => patchVisibleDeliveriesWithLiveUpdates(current, data.deliveries) ); diff --git a/apps/webapp/app/hooks/useEventSource.tsx b/apps/webapp/app/hooks/useEventSource.tsx index 4bcdac6f5..63c3b1873 100644 --- a/apps/webapp/app/hooks/useEventSource.tsx +++ b/apps/webapp/app/hooks/useEventSource.tsx @@ -24,6 +24,7 @@ export function useEventSource( } // reset data if dependencies change + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. setData(null); const eventSource = new EventSource(url, init); diff --git a/apps/webapp/app/hooks/useMetricResourceQuery.ts b/apps/webapp/app/hooks/useMetricResourceQuery.ts index f830fcb3e..8fa373837 100644 --- a/apps/webapp/app/hooks/useMetricResourceQuery.ts +++ b/apps/webapp/app/hooks/useMetricResourceQuery.ts @@ -214,6 +214,7 @@ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryO ]); useEffect(() => { + // oxlint-disable-next-line react/react-compiler -- This effect intentionally synchronizes local state after an external or lifecycle change. load(); return () => abortRef.current?.abort(); }, [load]);