From 6555da016a679c8e7113d140723bdbfabdfba3a0 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 17 Sep 2026 14:27:45 -0700 Subject: [PATCH 1/4] fix(issues): show Affected Users tooltip only when filtered stats are on The groupUsersCount tooltip tested the imported usePageFilters hook rather than the useFilteredStats prop its sibling groupCount block uses. A function reference is always truthy, so disabled was permanently false and the tooltip appeared even when the stream was not showing filtered stats. Also renames a useParamsValue test local to paramsValue; the use prefix made it read as a hook. Both were found by turning on oxlint's react/hooks rule. --- static/app/components/stream/group.tsx | 2 +- static/app/utils/useParams.spec.tsx | 20 ++++++++------------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/static/app/components/stream/group.tsx b/static/app/components/stream/group.tsx index 6ea5dc001743..33d5e3f9aedd 100644 --- a/static/app/components/stream/group.tsx +++ b/static/app/components/stream/group.tsx @@ -643,7 +643,7 @@ export function StreamGroup({ const groupUsersCount = (

{t('Affected Users')}

diff --git a/static/app/utils/useParams.spec.tsx b/static/app/utils/useParams.spec.tsx index 4dbcbb4a1794..af39d7a6b36c 100644 --- a/static/app/utils/useParams.spec.tsx +++ b/static/app/utils/useParams.spec.tsx @@ -72,16 +72,14 @@ describe('useParams', () => { mockCustomerDomain.mockReturnValue('albertos-apples'); let originalParams: any; - let useParamsValue: any; + let paramsValue: any; function Component() { // oxlint-disable-next-line react/globals -- Test captures the hook result in an outer variable to assert on it. originalParams = useReactRouter6Params(); // oxlint-disable-next-line react/globals -- Test captures the hook result in an outer variable to assert on it. - useParamsValue = useParams(); - return ( -
rendered component for org: {useParamsValue.orgId ?? 'no org id'}
- ); + paramsValue = useParams(); + return
rendered component for org: {paramsValue.orgId ?? 'no org id'}
; } render(, { @@ -95,7 +93,7 @@ describe('useParams', () => { screen.getByText('rendered component for org: albertos-apples') ).toBeInTheDocument(); expect(originalParams).toEqual({}); - expect(useParamsValue).toEqual({ + expect(paramsValue).toEqual({ orgId: 'albertos-apples', }); }); @@ -105,16 +103,14 @@ describe('useParams', () => { mockCustomerDomain.mockReturnValue(undefined); let originalParams: any; - let useParamsValue: any; + let paramsValue: any; function Component() { // oxlint-disable-next-line react/globals -- Test captures the hook result in an outer variable to assert on it. originalParams = useReactRouter6Params(); // oxlint-disable-next-line react/globals -- Test captures the hook result in an outer variable to assert on it. - useParamsValue = useParams(); - return ( -
rendered component for org: {useParamsValue.orgId ?? 'no org id'}
- ); + paramsValue = useParams(); + return
rendered component for org: {paramsValue.orgId ?? 'no org id'}
; } render(, { @@ -128,7 +124,7 @@ describe('useParams', () => { screen.getByText('rendered component for org: no org id') ).toBeInTheDocument(); expect(originalParams).toEqual({}); - expect(useParamsValue).toEqual({}); + expect(paramsValue).toEqual({}); }); }); }); From 06aea35c58da0f3f2378882df75edf58860380e4 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 17 Sep 2026 14:40:17 -0700 Subject: [PATCH 2/4] ref(lint): suppress the react/hooks sites that are deliberate indirection Every remaining react/hooks violation is a place where a hook is reached through a value rather than a static import, so React Compiler cannot prove the identity is stable. Three patterns cover all of them: - The override registry, which swaps a hook implementation for getsentry SaaS. Overrides register once before React renders, so the binding is fixed. - useProgressiveQuery, which takes the query hook as an argument so it can run it at more than one accuracy tier. - A dataset config or call-site flag selecting between query hooks, where the selector is constant for the lifetime of the component. None of these are fixable without redesigning the mechanism, so each site gets an oxlint-disable with the reason it is safe. Sites whose anchor is inside a wrapped expression use the trailing -disable-line form, because a preceding line cannot cover them and inserting one risks detaching a neighbouring directive. --- static/app/scrapsProviders/tracking.tsx | 7 +++++-- static/app/utils/provideAriaRouter.tsx | 6 +++++- static/app/utils/replays/useReplayForCriticalFlow.tsx | 2 +- static/app/utils/useExperiment.tsx | 4 ++-- static/app/utils/useMaxPickableDays.tsx | 10 +++++----- .../views/dashboards/hooks/useDatasetSearchBarData.tsx | 5 +++++ .../dashboards/widgetCard/genericWidgetQueries.tsx | 3 +++ static/app/views/dashboards/widgetCard/index.tsx | 1 + .../views/detectors/components/detectorTypeForm.tsx | 2 +- .../views/explore/hooks/useExploreAggregatesTable.tsx | 2 +- .../app/views/explore/hooks/useExploreSpansTable.tsx | 2 +- .../app/views/explore/hooks/useExploreTimeseries.tsx | 2 +- .../logs/exports/useLogsExportEstimatedRowCount.tsx | 2 +- .../app/views/explore/logs/useLogsAggregatesTable.tsx | 2 +- static/app/views/explore/logs/useLogsTimeseries.tsx | 2 +- .../explore/metrics/hooks/useMetricAggregatesTable.tsx | 2 +- .../explore/metrics/hooks/useMetricSamplesTable.tsx | 2 +- .../explore/metrics/hooks/useMetricTimeseries.tsx | 2 +- .../multiQueryMode/hooks/useMultiQueryTable.tsx | 4 ++-- .../multiQueryMode/hooks/useMultiQueryTimeseries.tsx | 2 +- .../app/views/insights/common/queries/useDiscover.ts | 8 ++++++-- static/app/views/issueDetails/eventMissingBanner.tsx | 2 +- static/app/views/issueDetails/header/header.tsx | 2 +- .../views/issueDetails/useGroupDefaultStatsPeriod.tsx | 2 +- static/app/views/routeAnalyticsContextProvider.tsx | 2 +- .../organization/organizationSettingsNavigation.tsx | 2 +- 26 files changed, 51 insertions(+), 31 deletions(-) diff --git a/static/app/scrapsProviders/tracking.tsx b/static/app/scrapsProviders/tracking.tsx index c63d4046d031..aa0a2af6e3b8 100644 --- a/static/app/scrapsProviders/tracking.tsx +++ b/static/app/scrapsProviders/tracking.tsx @@ -28,10 +28,13 @@ export function SentryTrackingProvider({children}: {children: React.ReactNode}) // Called here, not in `useClickTracking`, so a Button's hook count doesn't // depend on which implementation is registered. const useButtonTracking = - getOverride('react-hook:use-button-tracking') ?? useDefaultButtonTracking; + getOverride('react-hook:use-button-tracking') ?? useDefaultButtonTracking; // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. return ( - + {children} ); diff --git a/static/app/utils/provideAriaRouter.tsx b/static/app/utils/provideAriaRouter.tsx index 64e24579c43b..e0a62aa5780d 100644 --- a/static/app/utils/provideAriaRouter.tsx +++ b/static/app/utils/provideAriaRouter.tsx @@ -31,7 +31,11 @@ export function ProvideAriaRouter({children}: {children: React.ReactNode}) { ); return ( - + {children} ); diff --git a/static/app/utils/replays/useReplayForCriticalFlow.tsx b/static/app/utils/replays/useReplayForCriticalFlow.tsx index 55754733c606..1122a2b3a807 100644 --- a/static/app/utils/replays/useReplayForCriticalFlow.tsx +++ b/static/app/utils/replays/useReplayForCriticalFlow.tsx @@ -31,5 +31,5 @@ const noop = (_: UseReplayForCriticalFlowOptions) => {}; */ export function useReplayForCriticalFlow(options: UseReplayForCriticalFlowOptions) { const useImpl = getOverride('react-hook:use-replay-for-critical-flow') ?? noop; - useImpl(options); + useImpl(options); // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. } diff --git a/static/app/utils/useExperiment.tsx b/static/app/utils/useExperiment.tsx index 20a0f11b9df5..f62c930c49bd 100644 --- a/static/app/utils/useExperiment.tsx +++ b/static/app/utils/useExperiment.tsx @@ -87,6 +87,6 @@ function useNoopExperiment(options: UseExperimentOptions): UseExperimentResult { * ``` */ export function useExperiment(options: UseExperimentOptions): UseExperimentResult { - const useExperimentHook = getOverride('react-hook:use-experiment') ?? useNoopExperiment; - return useExperimentHook(options); + const useExperimentHook = getOverride('react-hook:use-experiment') ?? useNoopExperiment; // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. + return useExperimentHook(options); // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. } diff --git a/static/app/utils/useMaxPickableDays.tsx b/static/app/utils/useMaxPickableDays.tsx index 33ba700e4ecb..cf9db9457a23 100644 --- a/static/app/utils/useMaxPickableDays.tsx +++ b/static/app/utils/useMaxPickableDays.tsx @@ -16,9 +16,9 @@ import {useOrganization} from 'sentry/utils/useOrganization'; */ export function useDefaultMaxPickableDays(): number { const useDefaultMaxPickableDaysHook = - getOverride('react-hook:use-default-max-pickable-days') ?? - useDefaultMaxPickableDaysImpl; - return useDefaultMaxPickableDaysHook(); + getOverride('react-hook:use-default-max-pickable-days') ?? // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. + useDefaultMaxPickableDaysImpl; // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. + return useDefaultMaxPickableDaysHook(); // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. } function useDefaultMaxPickableDaysImpl() { @@ -50,8 +50,8 @@ export function useMaxPickableDays({ dataCategories, }: UseMaxPickableDaysProps): MaxPickableDaysOptions { const useMaxPickableDaysHook = - getOverride('react-hook:use-max-pickable-days') ?? useMaxPickableDaysImpl; - return useMaxPickableDaysHook({dataCategories}); + getOverride('react-hook:use-max-pickable-days') ?? useMaxPickableDaysImpl; // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. + return useMaxPickableDaysHook({dataCategories}); // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. } function useMaxPickableDaysImpl({dataCategories}: UseMaxPickableDaysProps) { diff --git a/static/app/views/dashboards/hooks/useDatasetSearchBarData.tsx b/static/app/views/dashboards/hooks/useDatasetSearchBarData.tsx index e9965ac81341..9211578b12ef 100644 --- a/static/app/views/dashboards/hooks/useDatasetSearchBarData.tsx +++ b/static/app/views/dashboards/hooks/useDatasetSearchBarData.tsx @@ -30,24 +30,29 @@ export function useDatasetSearchBarData(): { ? debouncedFilterKeySearch.query : undefined; + // oxlint-disable-next-line react/hooks -- Each call names a literal WidgetType, so the dataset config and its hook are fixed. const errorsData = getDatasetConfig(WidgetType.ERRORS).useSearchBarDataProvider!({ pageFilters: selection, }); + // oxlint-disable-next-line react/hooks -- Each call names a literal WidgetType, so the dataset config and its hook are fixed. const logsData = getDatasetConfig(WidgetType.LOGS).useSearchBarDataProvider!({ filterKeySearch: getFilterKeySearch(WidgetType.LOGS), pageFilters: selection, }); + // oxlint-disable-next-line react/hooks -- Each call names a literal WidgetType, so the dataset config and its hook are fixed. const spansData = getDatasetConfig(WidgetType.SPANS).useSearchBarDataProvider!({ filterKeySearch: getFilterKeySearch(WidgetType.SPANS), pageFilters: selection, }); + // oxlint-disable-next-line react/hooks -- Each call names a literal WidgetType, so the dataset config and its hook are fixed. const issuesData = getDatasetConfig(WidgetType.ISSUE).useSearchBarDataProvider!({ pageFilters: selection, }); + // oxlint-disable-next-line react/hooks -- Each call names a literal WidgetType, so the dataset config and its hook are fixed. const releasesData = getDatasetConfig(WidgetType.RELEASE).useSearchBarDataProvider!({ pageFilters: selection, }); diff --git a/static/app/views/dashboards/widgetCard/genericWidgetQueries.tsx b/static/app/views/dashboards/widgetCard/genericWidgetQueries.tsx index d40a41e85c1c..93316c51c3d0 100644 --- a/static/app/views/dashboards/widgetCard/genericWidgetQueries.tsx +++ b/static/app/views/dashboards/widgetCard/genericWidgetQueries.tsx @@ -206,6 +206,7 @@ export function useGenericWidgetQueries( [needsBreakdownTable, widget] ); + // oxlint-disable-next-line react/hooks -- Optional per-dataset query hook; the config prop must not change for a mounted card. const hookSeriesResults = config.useSeriesQuery?.({ widget, organization, @@ -221,6 +222,7 @@ export function useGenericWidgetQueries( widgetInterval, }); + // oxlint-disable-next-line react/hooks -- Optional per-dataset query hook; the config prop must not change for a mounted card. const hookTableResults = config.useTableQuery?.({ widget: tableWidget, organization, @@ -236,6 +238,7 @@ export function useGenericWidgetQueries( widgetInterval, }); + // oxlint-disable-next-line react/hooks -- Optional per-dataset query hook; the config prop must not change for a mounted card. const hookHeatmapResults = config.useHeatmapQuery?.({ widget, organization, diff --git a/static/app/views/dashboards/widgetCard/index.tsx b/static/app/views/dashboards/widgetCard/index.tsx index 0485252202c8..acf737bf7001 100644 --- a/static/app/views/dashboards/widgetCard/index.tsx +++ b/static/app/views/dashboards/widgetCard/index.tsx @@ -527,6 +527,7 @@ function useTimeRangeWarning({widget}: {widget: TWidget}) { } = usePageFilters(); const useRetentionLimit = getOverride('react-hook:use-dashboard-dataset-retention-limit') ?? (() => null); + // oxlint-disable-next-line react/hooks -- Hook comes from the override registry, which is populated before React renders. const retentionLimitDays = useRetentionLimit({ dataset: widget.widgetType ?? WidgetType.ERRORS, }); diff --git a/static/app/views/detectors/components/detectorTypeForm.tsx b/static/app/views/detectors/components/detectorTypeForm.tsx index 27814abfcb7c..3f913b480742 100644 --- a/static/app/views/detectors/components/detectorTypeForm.tsx +++ b/static/app/views/detectors/components/detectorTypeForm.tsx @@ -78,7 +78,7 @@ function MonitorTypeField() { const useMetricDetectorLimit = getOverride('react-hook:use-metric-detector-limit') ?? (() => null); - const quota = useMetricDetectorLimit(); + const quota = useMetricDetectorLimit(); // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. const canCreateMetricDetector = !quota?.hasReachedLimit; const handleChange = (value: SelectableDetectorType) => { diff --git a/static/app/views/explore/hooks/useExploreAggregatesTable.tsx b/static/app/views/explore/hooks/useExploreAggregatesTable.tsx index c6c782280b4b..603bf8489c6f 100644 --- a/static/app/views/explore/hooks/useExploreAggregatesTable.tsx +++ b/static/app/views/explore/hooks/useExploreAggregatesTable.tsx @@ -56,7 +56,7 @@ export function useExploreAggregatesTable({ [] ); return useProgressiveQuery({ - queryHookImplementation: useExploreAggregatesTableImp, + queryHookImplementation: useExploreAggregatesTableImp, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: {enabled, limit, query, queryExtras}, queryOptions: { canTriggerHighAccuracy, diff --git a/static/app/views/explore/hooks/useExploreSpansTable.tsx b/static/app/views/explore/hooks/useExploreSpansTable.tsx index 6964daacea31..8db99ebdd21e 100644 --- a/static/app/views/explore/hooks/useExploreSpansTable.tsx +++ b/static/app/views/explore/hooks/useExploreSpansTable.tsx @@ -121,7 +121,7 @@ export function useExploreSpansTable({ ); const spansTableResult = useProgressiveQuery({ - queryHookImplementation: useExploreSpansTableImp, + queryHookImplementation: useExploreSpansTableImp, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: { cursor: visibleSamples ? '' : undefined, enabled, diff --git a/static/app/views/explore/hooks/useExploreTimeseries.tsx b/static/app/views/explore/hooks/useExploreTimeseries.tsx index b2bc485d57a4..5ec61d8be1d6 100644 --- a/static/app/views/explore/hooks/useExploreTimeseries.tsx +++ b/static/app/views/explore/hooks/useExploreTimeseries.tsx @@ -58,7 +58,7 @@ export const useExploreTimeseries = ({ ); return useProgressiveQuery({ - queryHookImplementation: useExploreTimeseriesImpl, + queryHookImplementation: useExploreTimeseriesImpl, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: {query, enabled, queryExtras, includeAnnotations}, queryOptions: { canTriggerHighAccuracy, diff --git a/static/app/views/explore/logs/exports/useLogsExportEstimatedRowCount.tsx b/static/app/views/explore/logs/exports/useLogsExportEstimatedRowCount.tsx index 023e3583c762..fdbc56f63b19 100644 --- a/static/app/views/explore/logs/exports/useLogsExportEstimatedRowCount.tsx +++ b/static/app/views/explore/logs/exports/useLogsExportEstimatedRowCount.tsx @@ -53,7 +53,7 @@ export function useLogsExportEstimatedRowCount(tableDataLength: number) { const isTopN = !!baseRequest.topEvents; const timeseriesResult = useProgressiveQuery({ - queryHookImplementation: useLogsExportEstimateTimeseries, + queryHookImplementation: useLogsExportEstimateTimeseries, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: { baseRequest, enabled: true, diff --git a/static/app/views/explore/logs/useLogsAggregatesTable.tsx b/static/app/views/explore/logs/useLogsAggregatesTable.tsx index 58b7a1431c67..6cbfe9a4d10a 100644 --- a/static/app/views/explore/logs/useLogsAggregatesTable.tsx +++ b/static/app/views/explore/logs/useLogsAggregatesTable.tsx @@ -62,7 +62,7 @@ export function useLogsAggregatesTable({ const {result, pageLinks, eventView} = useProgressiveQuery< typeof useLogsAggregatesTableImpl >({ - queryHookImplementation: useLogsAggregatesTableImpl, + queryHookImplementation: useLogsAggregatesTableImpl, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: { enabled, limit, diff --git a/static/app/views/explore/logs/useLogsTimeseries.tsx b/static/app/views/explore/logs/useLogsTimeseries.tsx index 3fcde91431b4..710d9eca59ee 100644 --- a/static/app/views/explore/logs/useLogsTimeseries.tsx +++ b/static/app/views/explore/logs/useLogsTimeseries.tsx @@ -60,7 +60,7 @@ export function useLogsTimeseries({ ); const timeseriesResult = useProgressiveQuery({ - queryHookImplementation: useLogsTimeseriesImpl, + queryHookImplementation: useLogsTimeseriesImpl, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: {enabled, timeseriesIngestDelay}, queryOptions: { canTriggerHighAccuracy, diff --git a/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.tsx b/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.tsx index c415ff7d09bf..8e5dcd300fb4 100644 --- a/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.tsx +++ b/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.tsx @@ -109,7 +109,7 @@ export function useMetricAggregatesTable({ [traceMetric, visualize] ); return useProgressiveQuery({ - queryHookImplementation: useMetricAggregatesTableImp, + queryHookImplementation: useMetricAggregatesTableImp, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: { enabled, limit, diff --git a/static/app/views/explore/metrics/hooks/useMetricSamplesTable.tsx b/static/app/views/explore/metrics/hooks/useMetricSamplesTable.tsx index 4ad2d8d2488c..c777ffc29f68 100644 --- a/static/app/views/explore/metrics/hooks/useMetricSamplesTable.tsx +++ b/static/app/views/explore/metrics/hooks/useMetricSamplesTable.tsx @@ -244,7 +244,7 @@ export function useMetricSamplesTable({ ); return useProgressiveQuery({ - queryHookImplementation: useMetricSamplesTableImpl, + queryHookImplementation: useMetricSamplesTableImpl, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: { enabled: !disabled, limit, diff --git a/static/app/views/explore/metrics/hooks/useMetricTimeseries.tsx b/static/app/views/explore/metrics/hooks/useMetricTimeseries.tsx index 88c07a7dad2f..53f2a0a95f1f 100644 --- a/static/app/views/explore/metrics/hooks/useMetricTimeseries.tsx +++ b/static/app/views/explore/metrics/hooks/useMetricTimeseries.tsx @@ -35,7 +35,7 @@ export function useMetricTimeseries({traceMetric, enabled}: UseMetricTimeseriesO [topEvents, visualizes] ); return useProgressiveQuery({ - queryHookImplementation: useMetricTimeseriesImpl, + queryHookImplementation: useMetricTimeseriesImpl, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: {traceMetric, queryExtras: undefined, enabled}, queryOptions: { canTriggerHighAccuracy, diff --git a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.tsx b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.tsx index a4c3fef5ed2f..c8c7eb51aaf1 100644 --- a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.tsx +++ b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.tsx @@ -42,7 +42,7 @@ export function useMultiQueryTableAggregateMode({ [] ); return useProgressiveQuery({ - queryHookImplementation: useMultiQueryTableAggregateModeImpl, + queryHookImplementation: useMultiQueryTableAggregateModeImpl, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: {groupBys, query, yAxes, sortBys, enabled, queryExtras}, queryOptions: { canTriggerHighAccuracy, @@ -116,7 +116,7 @@ export function useMultiQueryTableSampleMode({ [] ); return useProgressiveQuery({ - queryHookImplementation: useMultiQueryTableSampleModeImpl, + queryHookImplementation: useMultiQueryTableSampleModeImpl, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: {query, yAxes, sortBys, enabled, queryExtras}, queryOptions: { canTriggerHighAccuracy, diff --git a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.tsx b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.tsx index cc3634944fff..ce975529e42c 100644 --- a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.tsx +++ b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.tsx @@ -54,7 +54,7 @@ export function useMultiQueryTimeseries({ [] ); return useProgressiveQuery({ - queryHookImplementation: useMultiQueryTimeseriesImpl, + queryHookImplementation: useMultiQueryTimeseriesImpl, // oxlint-disable-line react/hooks -- useProgressiveQuery takes the query hook as a value and calls it per accuracy tier. queryHookArgs: {enabled, index, queryExtras}, queryOptions: { canTriggerHighAccuracy, diff --git a/static/app/views/insights/common/queries/useDiscover.ts b/static/app/views/insights/common/queries/useDiscover.ts index 3f18aa0312de..28c211533f19 100644 --- a/static/app/views/insights/common/queries/useDiscover.ts +++ b/static/app/views/insights/common/queries/useDiscover.ts @@ -80,10 +80,14 @@ const useDiscover = >, Respo projectIds ); + // oxlint-disable-next-line react/hooks -- queryWithoutPageFilters is a constant per call site, so the branch never flips. const queryFn = options.queryWithoutPageFilters - ? useWrappedDiscoverQueryWithoutPageFilters - : useWrappedDiscoverQuery; + ? // oxlint-disable-next-line react/hooks -- queryWithoutPageFilters is a constant per call site, so the branch never flips. + useWrappedDiscoverQueryWithoutPageFilters + : // oxlint-disable-next-line react/hooks -- queryWithoutPageFilters is a constant per call site, so the branch never flips. + useWrappedDiscoverQuery; + // oxlint-disable-next-line react/hooks -- queryWithoutPageFilters is a constant per call site, so the branch never flips. const result = queryFn({ eventView, initialData: [], diff --git a/static/app/views/issueDetails/eventMissingBanner.tsx b/static/app/views/issueDetails/eventMissingBanner.tsx index fd3d984f55dc..920394dcb801 100644 --- a/static/app/views/issueDetails/eventMissingBanner.tsx +++ b/static/app/views/issueDetails/eventMissingBanner.tsx @@ -26,7 +26,7 @@ export function EventMissingBanner() { const useGetMaxRetentionDays = getOverride('react-hook:use-get-max-retention-days') ?? (() => MAX_PICKABLE_DAYS); - const maxRetentionDays = useGetMaxRetentionDays(); + const maxRetentionDays = useGetMaxRetentionDays(); // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. const statsPeriod = maxRetentionDays ? `${maxRetentionDays}d` : '30d'; const baseUrl = `/organizations/${organization.slug}/issues/${groupId}/events`; diff --git a/static/app/views/issueDetails/header/header.tsx b/static/app/views/issueDetails/header/header.tsx index 49d4c9c376ed..a36e7b6e61b2 100644 --- a/static/app/views/issueDetails/header/header.tsx +++ b/static/app/views/issueDetails/header/header.tsx @@ -57,7 +57,7 @@ export function GroupHeader({event, group, project}: GroupHeaderProps) { const {count: eventCount, userCount} = group; const useGetMaxRetentionDays = getOverride('react-hook:use-get-max-retention-days') ?? (() => MAX_PICKABLE_DAYS); - const maxRetentionDays = useGetMaxRetentionDays(); + const maxRetentionDays = useGetMaxRetentionDays(); // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. const userCountPeriod = maxRetentionDays ? `(${maxRetentionDays}d)` : '(30d)'; const {title: primaryTitle} = getTitle(group); const secondaryTitle = getMessage(group); diff --git a/static/app/views/issueDetails/useGroupDefaultStatsPeriod.tsx b/static/app/views/issueDetails/useGroupDefaultStatsPeriod.tsx index 5dadadf53c8b..ba3fe614f332 100644 --- a/static/app/views/issueDetails/useGroupDefaultStatsPeriod.tsx +++ b/static/app/views/issueDetails/useGroupDefaultStatsPeriod.tsx @@ -39,7 +39,7 @@ export function useGroupDefaultStatsPeriod( ): UseGroupDefaultStatsPeriodResult { const useGetMaxRetentionDays = getOverride('react-hook:use-get-max-retention-days') ?? (() => MAX_PICKABLE_DAYS); - const maxRetentionDays = useGetMaxRetentionDays(); + const maxRetentionDays = useGetMaxRetentionDays(); // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. let isMaxRetention = false; if (!group) { diff --git a/static/app/views/routeAnalyticsContextProvider.tsx b/static/app/views/routeAnalyticsContextProvider.tsx index 3cc298a3aace..aa28138efd29 100644 --- a/static/app/views/routeAnalyticsContextProvider.tsx +++ b/static/app/views/routeAnalyticsContextProvider.tsx @@ -48,7 +48,7 @@ export function RouteAnalyticsContextProvider({children}: Props) { setOrganization, setEventNames, previousUrl, - } = useRouteActivatedHook?.(context) || DEFAULT_CONTEXT; + } = useRouteActivatedHook?.(context) || DEFAULT_CONTEXT; // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. const memoizedValue = useMemo( () => ({ diff --git a/static/app/views/settings/organization/organizationSettingsNavigation.tsx b/static/app/views/settings/organization/organizationSettingsNavigation.tsx index bf8226130b3a..fc289b82a5b4 100644 --- a/static/app/views/settings/organization/organizationSettingsNavigation.tsx +++ b/static/app/views/settings/organization/organizationSettingsNavigation.tsx @@ -8,7 +8,7 @@ function OrganizationSettingsNavigation() { const organization = useOrganization(); const useBillingNavConfig = getOverride('react-hook:use-billing-navigation-config') ?? (() => null); - const billingNavConfig = useBillingNavConfig(); + const billingNavConfig = useBillingNavConfig(); // oxlint-disable-line react/hooks -- Hook comes from the override registry, which is populated before React renders. return ( Date: Thu, 17 Sep 2026 14:43:14 -0700 Subject: [PATCH 3/4] ref(lint): promote react/hooks to an error Now that the tree is clean, turning the rule on keeps the remaining hook indirection from spreading. Each existing exception carries a written reason, so a new violation has to argue for itself rather than land silently. --- oxlint.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oxlint.config.ts b/oxlint.config.ts index adf70249b989..e689f9d3e3e3 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -638,7 +638,7 @@ const config = defineConfig({ 'react/exhaustive-effect-dependencies': 'error', 'react/function-component-definition': 'error', 'react/globals': 'error', - 'react/hooks': 'off', // TODO(ryan953): Fix violations and promote this warning to an error. + 'react/hooks': 'error', 'react/immutability': 'error', 'react/incompatible-library': 'off', // TODO(ryan953): Fix violations and promote this warning to an error. 'react/invariant': 'error', From f72b6b2173156f787993c03a74e8da8bcb13c0d6 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 17 Sep 2026 15:06:05 -0700 Subject: [PATCH 4/4] Apply batched suggestions from code review Co-authored-by: Ryan Albrecht --- static/app/components/stream/group.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/components/stream/group.tsx b/static/app/components/stream/group.tsx index 33d5e3f9aedd..3f80c4325009 100644 --- a/static/app/components/stream/group.tsx +++ b/static/app/components/stream/group.tsx @@ -643,7 +643,6 @@ export function StreamGroup({ const groupUsersCount = (

{t('Affected Users')}