Skip to content

Commit e3b7d0a

Browse files
feat(eslint): no-unnecessary-use-callback (#112689)
This PR adds a new lint rule, `no-unnecessary-use-callback`, that only checks if `useCallback` is unnecessary in 3 scenarios: 1. The only usage is a direct invocation: ``` const fn = useCallback(() => ..., [deps]) <Component onClick={() => fn()} /> ``` This is unnecessary because the referential stability of `fn` is never “consumed”. 2. They are passed to react intrinsic elements ``` const fn = useCallback(() => ..., [deps]) <button onClick={fn} /> ``` React intrinsic elements like `button` _never_ expect props to be memoized, the components themselves are not memoized so the `useCallback` does nothing. 3. They pare passed to `scraps` components. Scraps components, too, don’t expect consumers to memoize input: ``` const fn = useCallback(() => ..., [deps]) <LinkButton onClick={fn} /> ``` all other cases are left as they are. Still, there were 194 violations of this new rule, which were auto fixed. --------- Co-authored-by: Josh Goldberg ✨ <github@joshuakgoldberg.com>
1 parent 670a97d commit e3b7d0a

File tree

146 files changed

+2684
-2372
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

146 files changed

+2684
-2372
lines changed

eslint.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ export default typescript.config([
462462
'@sentry/no-flag-comments': 'error',
463463
'@sentry/no-static-translations': 'error',
464464
'@sentry/no-styled-shortcut': 'error',
465+
'@sentry/no-unnecessary-use-callback': 'error',
465466
},
466467
},
467468
{

static/app/components/assistant/guideAnchor.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,6 @@ function BaseGuideAnchor({
105105
[onStepComplete]
106106
);
107107

108-
const handleDismiss = useCallback(
109-
(e: React.MouseEvent) => {
110-
e.stopPropagation();
111-
if (currentGuide) {
112-
dismissGuide(currentGuide.guide, step, orgId);
113-
}
114-
},
115-
[currentGuide, orgId, step]
116-
);
117-
118108
if (!active) {
119109
return children ? children : null;
120110
}
@@ -133,7 +123,10 @@ function BaseGuideAnchor({
133123
stepCount={currentStepCount}
134124
stepTotal={totalStepCount}
135125
handleDismiss={e => {
136-
handleDismiss(e);
126+
e.stopPropagation();
127+
if (currentGuide) {
128+
dismissGuide(currentGuide.guide, step, orgId);
129+
}
137130
window.location.hash = '';
138131
}}
139132
actions={

static/app/components/avatarChooser/useUploader.tsx

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,34 +41,31 @@ export function useUploader({onSelect, minImageSize}: UseUploaderOptions) {
4141
[minImageSize]
4242
);
4343

44-
const onSelectFile = useCallback(
45-
async (ev: React.ChangeEvent<HTMLInputElement>) => {
46-
const file = ev.target.files?.[0];
44+
const onSelectFile = async (ev: React.ChangeEvent<HTMLInputElement>) => {
45+
const file = ev.target.files?.[0];
4746

48-
// No file selected (e.g. user clicked "cancel")
49-
if (!file) {
50-
return;
51-
}
47+
// No file selected (e.g. user clicked "cancel")
48+
if (!file) {
49+
return;
50+
}
5251

53-
if (!/^image\//.test(file.type)) {
54-
addErrorMessage(t('That is not a supported file type.'));
55-
return;
56-
}
57-
const url = window.URL.createObjectURL(file);
58-
const {height, width} = await getImageHeightAndWidth(url);
59-
const sizeValidation = validateSize(height, width);
52+
if (!/^image\//.test(file.type)) {
53+
addErrorMessage(t('That is not a supported file type.'));
54+
return;
55+
}
56+
const url = window.URL.createObjectURL(file);
57+
const {height, width} = await getImageHeightAndWidth(url);
58+
const sizeValidation = validateSize(height, width);
6059

61-
if (sizeValidation !== true) {
62-
addErrorMessage(sizeValidation);
63-
return;
64-
}
60+
if (sizeValidation !== true) {
61+
addErrorMessage(sizeValidation);
62+
return;
63+
}
6564

66-
setObjectUrl(url);
67-
onSelect(url);
68-
ev.target.value = '';
69-
},
70-
[onSelect, validateSize]
71-
);
65+
setObjectUrl(url);
66+
onSelect(url);
67+
ev.target.value = '';
68+
};
7269

7370
const fileInput = (
7471
<input

static/app/components/clippedBox.tsx

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -161,51 +161,45 @@ export function ClippedBox(props: ClippedBoxProps) {
161161
const clipHeight = props.clipHeight || 200;
162162
const clipFlex = props.clipFlex || 28;
163163

164-
const handleReveal = useCallback(
165-
(event: React.MouseEvent<HTMLElement>) => {
166-
if (!wrapperRef.current) {
167-
throw new Error('Cannot reveal clipped box without a wrapper ref');
168-
}
169-
170-
event.stopPropagation();
171-
172-
revealAndDisconnectObserver({
173-
contentRef,
174-
wrapperRef,
175-
revealRef,
176-
observerRef,
177-
clipHeight,
178-
prefersReducedMotion: prefersReducedMotion ?? true,
179-
});
180-
if (typeof onReveal === 'function') {
181-
onReveal();
164+
const handleReveal = (event: React.MouseEvent<HTMLElement>) => {
165+
if (!wrapperRef.current) {
166+
throw new Error('Cannot reveal clipped box without a wrapper ref');
167+
}
168+
169+
event.stopPropagation();
170+
171+
revealAndDisconnectObserver({
172+
contentRef,
173+
wrapperRef,
174+
revealRef,
175+
observerRef,
176+
clipHeight,
177+
prefersReducedMotion: prefersReducedMotion ?? true,
178+
});
179+
if (typeof onReveal === 'function') {
180+
onReveal();
181+
}
182+
183+
setClipped(false);
184+
};
185+
186+
const handleCollapse = (event: React.MouseEvent<HTMLElement>) => {
187+
event.stopPropagation();
188+
189+
if (wrapperRef.current && contentRef.current) {
190+
if (prefersReducedMotion) {
191+
wrapperRef.current.style.maxHeight = `${clipHeight}px`;
192+
} else {
193+
const currentHeight =
194+
contentRef.current.clientHeight + calculateAddedHeight({wrapperRef});
195+
wrapperRef.current.style.maxHeight = `${currentHeight}px`;
196+
void wrapperRef.current.offsetHeight;
197+
wrapperRef.current.style.maxHeight = `${clipHeight}px`;
182198
}
183-
184-
setClipped(false);
185-
},
186-
[clipHeight, onReveal, prefersReducedMotion]
187-
);
188-
189-
const handleCollapse = useCallback(
190-
(event: React.MouseEvent<HTMLElement>) => {
191-
event.stopPropagation();
192-
193-
if (wrapperRef.current && contentRef.current) {
194-
if (prefersReducedMotion) {
195-
wrapperRef.current.style.maxHeight = `${clipHeight}px`;
196-
} else {
197-
const currentHeight =
198-
contentRef.current.clientHeight + calculateAddedHeight({wrapperRef});
199-
wrapperRef.current.style.maxHeight = `${currentHeight}px`;
200-
void wrapperRef.current.offsetHeight;
201-
wrapperRef.current.style.maxHeight = `${clipHeight}px`;
202-
}
203-
}
204-
revealRef.current = false;
205-
setClipped(true);
206-
},
207-
[clipHeight, prefersReducedMotion]
208-
);
199+
}
200+
revealRef.current = false;
201+
setClipped(true);
202+
};
209203

210204
const onWrapperRef = useCallback(
211205
(node: HTMLDivElement | null) => {

static/app/components/core/input/numberDragInput.tsx

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -78,40 +78,34 @@ export function NumberDragInput({
7878
document.removeEventListener('pointerup', onPointerUp);
7979
}, [onPointerMove]);
8080

81-
const onPointerDown = useCallback(
82-
(event: React.PointerEvent<HTMLElement>) => {
83-
if (event.button !== 0) {
84-
return;
85-
}
86-
87-
// Request pointer lock and add move and pointer up handlers
88-
// that release the lock and cleanup handlers
89-
event.currentTarget.requestPointerLock();
90-
document.addEventListener('pointermove', onPointerMove);
91-
document.addEventListener('pointerup', onPointerUp);
92-
},
93-
[onPointerMove, onPointerUp]
94-
);
81+
const onPointerDown = (event: React.PointerEvent<HTMLElement>) => {
82+
if (event.button !== 0) {
83+
return;
84+
}
85+
86+
// Request pointer lock and add move and pointer up handlers
87+
// that release the lock and cleanup handlers
88+
event.currentTarget.requestPointerLock();
89+
document.addEventListener('pointermove', onPointerMove);
90+
document.addEventListener('pointerup', onPointerUp);
91+
};
9592

9693
const onKeyDownProp = props.onKeyDown;
97-
const onKeyDown = useCallback(
98-
(event: React.KeyboardEvent<HTMLInputElement>) => {
99-
onKeyDownProp?.(event);
100-
101-
if (!inputRef.current || (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')) {
102-
return;
103-
}
104-
105-
event.preventDefault();
106-
const value = parseFloat(inputRef.current.value);
107-
const step = parseFloat(props.step?.toString() ?? '1');
108-
const min = props.min ?? Number.NEGATIVE_INFINITY;
109-
const max = props.max ?? Number.POSITIVE_INFINITY;
110-
const newValue = clamp(value + (event.key === 'ArrowUp' ? step : -step), min, max);
111-
setInputValueAndDispatchChange(inputRef.current, newValue.toString());
112-
},
113-
[onKeyDownProp, props.min, props.max, props.step]
114-
);
94+
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
95+
onKeyDownProp?.(event);
96+
97+
if (!inputRef.current || (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')) {
98+
return;
99+
}
100+
101+
event.preventDefault();
102+
const value = parseFloat(inputRef.current.value);
103+
const step = parseFloat(props.step?.toString() ?? '1');
104+
const min = props.min ?? Number.NEGATIVE_INFINITY;
105+
const max = props.max ?? Number.POSITIVE_INFINITY;
106+
const newValue = clamp(value + (event.key === 'ArrowUp' ? step : -step), min, max);
107+
setInputValueAndDispatchChange(inputRef.current, newValue.toString());
108+
};
115109

116110
return (
117111
<InputGroup>

static/app/components/core/inspector.tsx

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -282,25 +282,22 @@ export function SentryComponentInspector() {
282282
}, [state.trace]);
283283

284284
const {ref: contextMenuRef, ...contextMenuProps} = {...contextMenu.getMenuProps()};
285-
const positionContextMenuOnMountRef = useCallback(
286-
(ref: HTMLDivElement | null) => {
287-
contextMenuRef(ref);
288-
289-
if (ref) {
290-
const position = computeTooltipPosition(
291-
{
292-
x: tooltipPositionRef.current?.mouse.x ?? 0,
293-
y: tooltipPositionRef.current?.mouse.y ?? 0,
294-
},
295-
ref
296-
);
285+
const positionContextMenuOnMountRef = (ref: HTMLDivElement | null) => {
286+
contextMenuRef(ref);
287+
288+
if (ref) {
289+
const position = computeTooltipPosition(
290+
{
291+
x: tooltipPositionRef.current?.mouse.x ?? 0,
292+
y: tooltipPositionRef.current?.mouse.y ?? 0,
293+
},
294+
ref
295+
);
297296

298-
ref.style.left = `${position.left}px`;
299-
ref.style.top = `${position.top}px`;
300-
}
301-
},
302-
[contextMenuRef]
303-
);
297+
ref.style.left = `${position.left}px`;
298+
ref.style.top = `${position.top}px`;
299+
}
300+
};
304301

305302
const storybookFiles = useStoryBookFiles();
306303
const storybookFilesLookup = useMemo(

static/app/components/events/autofix/autofixStepFeedback.tsx

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {useCallback, useState} from 'react';
1+
import {useState} from 'react';
22

3-
import {Button} from '@sentry/scraps/button';
43
import type {ButtonProps} from '@sentry/scraps/button';
4+
import {Button} from '@sentry/scraps/button';
55
import {Flex} from '@sentry/scraps/layout';
66
import {Text} from '@sentry/scraps/text';
77

@@ -34,27 +34,24 @@ export function AutofixStepFeedback({
3434
const organization = useOrganization();
3535
const user = useUser();
3636

37-
const handleFeedback = useCallback(
38-
(positive: boolean, e?: React.MouseEvent) => {
39-
if (onFeedbackClick && e) {
40-
onFeedbackClick(e);
41-
}
37+
const handleFeedback = (positive: boolean, e?: React.MouseEvent) => {
38+
if (onFeedbackClick && e) {
39+
onFeedbackClick(e);
40+
}
4241

43-
const analyticsData = {
44-
step_type: stepType,
45-
positive,
46-
group_id: groupId,
47-
autofix_run_id: runId,
48-
user_id: user.id,
49-
organization,
50-
};
42+
const analyticsData = {
43+
step_type: stepType,
44+
positive,
45+
group_id: groupId,
46+
autofix_run_id: runId,
47+
user_id: user.id,
48+
organization,
49+
};
5150

52-
trackAnalytics('seer.autofix.feedback_submitted', analyticsData);
51+
trackAnalytics('seer.autofix.feedback_submitted', analyticsData);
5352

54-
setFeedbackSubmitted(true);
55-
},
56-
[stepType, groupId, runId, organization, user, onFeedbackClick]
57-
);
53+
setFeedbackSubmitted(true);
54+
};
5855

5956
if (feedbackSubmitted) {
6057
return (

static/app/components/events/autofix/codingAgentIntegrationCta.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import {useCallback} from 'react';
2-
31
import {Button, LinkButton} from '@sentry/scraps/button';
42
import {Container, Flex} from '@sentry/scraps/layout';
53
import {ExternalLink, Link} from '@sentry/scraps/link';
@@ -61,17 +59,17 @@ export function makeCodingAgentIntegrationCta(config: AgentConfig) {
6159
const isConfigured =
6260
preference?.automation_handoff?.target === config.target && isAutomationEnabled;
6361

64-
const handleInstallClick = useCallback(() => {
62+
const handleInstallClick = () => {
6563
trackAnalytics('coding_integration.install_clicked', {
6664
organization,
6765
project_slug: project.slug,
6866
provider: config.provider,
6967
source: 'cta',
7068
user_id: user.id,
7169
});
72-
}, [organization, project.slug, user.id]);
70+
};
7371

74-
const handleSetupClick = useCallback(async () => {
72+
const handleSetupClick = async () => {
7573
if (!integration?.id) {
7674
throw new Error(`${config.displayName} integration not found`);
7775
}
@@ -104,17 +102,7 @@ export function makeCodingAgentIntegrationCta(config: AgentConfig) {
104102
integration_id: parseInt(integration.id, 10),
105103
},
106104
});
107-
}, [
108-
organization,
109-
project.slug,
110-
project.seerScannerAutomation,
111-
project.autofixAutomationTuning,
112-
updateProjectSeerPreferences,
113-
updateProjectAutomation,
114-
preference?.repositories,
115-
integration,
116-
user.id,
117-
]);
105+
};
118106

119107
if (!hasFeatureFlag) {
120108
return null;

0 commit comments

Comments
 (0)