Skip to content

Commit c2964b5

Browse files
TkDodoscttcper
andauthored
ref: replace forwardRef with ref prop (#86872)
This PR replaces all usages of `forwardRef` with a regular `ref` prop, as this is now possible with React 19. Most of the changes were made by a codemod (provided by react), but some places had to be changed manually. Further, some forwardRef removals yielded type-errors that were previously swallowed because of how we’ve typed generic forwardRefs (especially for the `Select` component). This needs to be addressed in a follow-up. I’ve also enabled a lint rule that forbids importing `forwardRef` and using `React.forwardRef` (for cases where we `import * as React from 'react`). This should make sure that no further `forwardRef` usages make it into the codease. --------- Co-authored-by: Scott Cooper <[email protected]>
1 parent 735cc30 commit c2964b5

File tree

227 files changed

+3260
-3331
lines changed

Some content is hidden

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

227 files changed

+3260
-3331
lines changed

eslint.config.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,21 @@ export default typescript.config([
275275
paths: restrictedImportPaths,
276276
},
277277
],
278+
'no-restricted-syntax': [
279+
'error',
280+
{
281+
selector:
282+
"ImportDeclaration[source.value='react'] > ImportSpecifier[imported.name='forwardRef']",
283+
message:
284+
'Since React 19, it is no longer necessary to use forwardRef - refs can be passed as a normal prop',
285+
},
286+
{
287+
selector:
288+
"CallExpression[callee.object.name='React'][callee.property.name='forwardRef']",
289+
message:
290+
'Since React 19, it is no longer necessary to use forwardRef - refs can be passed as a normal prop',
291+
},
292+
],
278293
'no-return-assign': 'error',
279294
'no-script-url': 'error',
280295
'no-self-compare': 'error',

static/app/components/ai/SeerIcon.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {forwardRef} from 'react';
21
import {keyframes} from '@emotion/react';
32
import styled from '@emotion/styled';
43

@@ -55,7 +54,7 @@ const InteractionWrapper = styled('div')`
5554
}
5655
`;
5756

58-
const SeerIcon = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) => {
57+
function SeerIcon({ref, ...props}: SVGIconProps) {
5958
return (
6059
<SvgIcon {...props} ref={ref} viewBox="0 0 30 30" kind="path">
6160
<path
@@ -87,11 +86,11 @@ const SeerIcon = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) => {
8786
/>
8887
</SvgIcon>
8988
);
90-
});
89+
}
9190

9291
SeerIcon.displayName = 'SeerIcon';
9392

94-
const SeerLoadingIcon = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) => {
93+
function SeerLoadingIcon({ref, ...props}: SVGIconProps) {
9594
return (
9695
<InteractionWrapper>
9796
<SvgIcon {...props} ref={ref} viewBox="0 0 30 30" kind="path">
@@ -151,9 +150,9 @@ const SeerLoadingIcon = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) =>
151150
</SvgIcon>
152151
</InteractionWrapper>
153152
);
154-
});
153+
}
155154

156-
const SeerWaitingIcon = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) => {
155+
function SeerWaitingIcon({ref, ...props}: SVGIconProps) {
157156
return (
158157
<InteractionWrapper>
159158
<SvgIcon {...props} ref={ref} viewBox="0 0 30 30" kind="path">
@@ -214,6 +213,6 @@ const SeerWaitingIcon = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) =>
214213
</SvgIcon>
215214
</InteractionWrapper>
216215
);
217-
});
216+
}
218217

219218
export {SeerIcon, SeerLoadingIcon, SeerWaitingIcon};

static/app/components/autoSelectText.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
1-
import {forwardRef, useImperativeHandle, useRef} from 'react';
1+
import {useImperativeHandle, useRef} from 'react';
22
import classNames from 'classnames';
33

44
import {selectText} from 'sentry/utils/selectText';
55

66
type Props = {
77
children: React.ReactNode;
88
className?: string;
9+
ref?: React.Ref<AutoSelectHandle>;
910
style?: React.CSSProperties;
1011
};
1112

1213
type AutoSelectHandle = {
1314
selectText: () => void;
1415
};
1516

16-
function AutoSelectText(
17-
{children, className, ...props}: Props,
18-
forwardedRef: React.Ref<AutoSelectHandle>
19-
) {
17+
function AutoSelectText({children, className, ref, ...props}: Props) {
2018
const element = useRef<HTMLSpanElement>(null);
2119

2220
// We need to expose a selectText method to parent components
2321
// and need an imperative ref handle.
24-
useImperativeHandle(forwardedRef, () => ({
22+
useImperativeHandle(ref, () => ({
2523
selectText: () => handleClick(),
2624
}));
2725

@@ -47,4 +45,4 @@ function AutoSelectText(
4745
);
4846
}
4947

50-
export default forwardRef(AutoSelectText);
48+
export default AutoSelectText;

static/app/components/badge/iconCellSignal.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import {forwardRef} from 'react';
21
import {useTheme} from '@emotion/react';
32

43
import {SvgIcon, type SVGIconProps} from 'sentry/icons/svgIcon';
54

65
interface Props extends SVGIconProps {
76
bars?: 0 | 1 | 2 | 3;
87
}
9-
const IconCellSignal = forwardRef<SVGSVGElement, Props>(({bars = 3, ...props}, ref) => {
8+
function IconCellSignal({ref, bars = 3, ...props}: Props) {
109
const theme = useTheme();
1110
const firstBarColor = bars > 0 ? theme.gray300 : theme.gray200;
1211
const secondBarColor = bars > 1 ? theme.gray300 : theme.gray200;
@@ -19,7 +18,7 @@ const IconCellSignal = forwardRef<SVGSVGElement, Props>(({bars = 3, ...props}, r
1918
<rect x="12.4" y="0" width="4" height="15" fill={thirdBarColor} rx="1" />
2019
</SvgIcon>
2120
);
22-
});
21+
}
2322

2423
IconCellSignal.displayName = 'IconCellSignal';
2524

static/app/components/charts/areaChart.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {forwardRef} from 'react';
21
import type {LineSeriesOption} from 'echarts';
32

43
import type {ReactEchartsRef, Series} from 'sentry/types/echarts';
@@ -47,16 +46,22 @@ export function transformToAreaSeries({
4746
);
4847
}
4948

50-
export const AreaChart = forwardRef<ReactEchartsRef, AreaChartProps>(
51-
({series, stacked, colors, ...props}, ref) => {
52-
return (
53-
<BaseChart
54-
{...props}
55-
ref={ref}
56-
data-test-id="area-chart"
57-
colors={colors}
58-
series={transformToAreaSeries({series, stacked, colors})}
59-
/>
60-
);
61-
}
62-
);
49+
export function AreaChart({
50+
ref,
51+
series,
52+
stacked,
53+
colors,
54+
...props
55+
}: AreaChartProps & {
56+
ref?: React.Ref<ReactEchartsRef>;
57+
}) {
58+
return (
59+
<BaseChart
60+
{...props}
61+
ref={ref}
62+
data-test-id="area-chart"
63+
colors={colors}
64+
series={transformToAreaSeries({series, stacked, colors})}
65+
/>
66+
);
67+
}

static/app/components/charts/barChart.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {forwardRef, useMemo} from 'react';
1+
import {useMemo} from 'react';
22
import type {BarSeriesOption} from 'echarts';
33

44
import type {ReactEchartsRef, Series} from 'sentry/types/echarts';
@@ -60,19 +60,28 @@ export function transformToBarSeries({
6060
}
6161

6262
const EMPTY_AXIS = {};
63-
export const BarChart = forwardRef<ReactEchartsRef, BarChartProps>(
64-
({barOpacity, hideZeros, series, stacked, xAxis, animation, ...props}, ref) => {
65-
const transformedSeries = useMemo(() => {
66-
return transformToBarSeries({barOpacity, hideZeros, series, stacked, animation});
67-
}, [animation, barOpacity, hideZeros, series, stacked]);
63+
export function BarChart({
64+
ref,
65+
barOpacity,
66+
hideZeros,
67+
series,
68+
stacked,
69+
xAxis,
70+
animation,
71+
...props
72+
}: BarChartProps & {
73+
ref?: React.Ref<ReactEchartsRef>;
74+
}) {
75+
const transformedSeries = useMemo(() => {
76+
return transformToBarSeries({barOpacity, hideZeros, series, stacked, animation});
77+
}, [animation, barOpacity, hideZeros, series, stacked]);
6878

69-
const xAxisOptions = useMemo(() => {
70-
const option = xAxis === null ? null : {...(xAxis || EMPTY_AXIS)};
71-
return option;
72-
}, [xAxis]);
79+
const xAxisOptions = useMemo(() => {
80+
const option = xAxis === null ? null : {...(xAxis || EMPTY_AXIS)};
81+
return option;
82+
}, [xAxis]);
7383

74-
return (
75-
<BaseChart {...props} ref={ref} xAxis={xAxisOptions} series={transformedSeries} />
76-
);
77-
}
78-
);
84+
return (
85+
<BaseChart {...props} ref={ref} xAxis={xAxisOptions} series={transformedSeries} />
86+
);
87+
}

static/app/components/charts/baseChart.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import 'echarts/lib/component/toolbox';
44
import 'echarts/lib/component/brush';
55
import 'zrender/lib/svg/svg';
66

7-
import {forwardRef, useMemo} from 'react';
7+
import {useMemo} from 'react';
88
import type {Theme} from '@emotion/react';
99
import {css, Global, useTheme} from '@emotion/react';
1010
import styled from '@emotion/styled';
@@ -621,17 +621,21 @@ function BaseChartUnwrapped({
621621
handleClick(props, instance);
622622
onClick?.(props, instance);
623623
},
624+
624625
highlight: (props: any, instance: ECharts) => onHighlight?.(props, instance),
625626
mouseout: (props: any, instance: ECharts) => onMouseOut?.(props, instance),
626627
mouseover: (props: any, instance: ECharts) => onMouseOver?.(props, instance),
627628
datazoom: (props: any, instance: ECharts) => onDataZoom?.(props, instance),
628629
restore: (props: any, instance: ECharts) => onRestore?.(props, instance),
629630
finished: (props: any, instance: ECharts) => onFinished?.(props, instance),
630631
rendered: (props: any, instance: ECharts) => onRendered?.(props, instance),
632+
631633
legendselectchanged: (props: any, instance: ECharts) =>
632634
onLegendSelectChanged?.(props, instance),
635+
633636
brush: (props: any, instance: ECharts) => onBrushStart?.(props, instance),
634637
brushend: (props: any, instance: ECharts) => onBrushEnd?.(props, instance),
638+
635639
brushselected: (props: any, instance: ECharts) =>
636640
onBrushSelected?.(props, instance),
637641
}) as ReactEchartProps['onEvents'],
@@ -849,9 +853,14 @@ const getPortalledTooltipStyles = (p: {theme: Theme}) => css`
849853
}
850854
`;
851855

852-
const BaseChart = forwardRef<ReactEchartsRef, BaseChartProps>((props, ref) => (
853-
<BaseChartUnwrapped forwardedRef={ref} {...props} />
854-
));
856+
function BaseChart({
857+
ref,
858+
...props
859+
}: BaseChartProps & {
860+
ref?: React.Ref<ReactEchartsRef>;
861+
}) {
862+
return <BaseChartUnwrapped forwardedRef={ref} {...props} />;
863+
}
855864

856865
BaseChart.displayName = 'forwardRef(BaseChart)';
857866

static/app/components/charts/heatMapChart.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import './components/visualMap';
22

3-
import {forwardRef} from 'react';
43
import type {HeatmapSeriesOption, VisualMapComponentOption} from 'echarts';
54

65
import type {ReactEchartsRef, Series} from 'sentry/types/echarts';
@@ -21,7 +20,12 @@ interface HeatmapProps extends Omit<BaseChartProps, 'series'> {
2120
seriesOptions?: HeatmapSeriesOption;
2221
}
2322

24-
export default forwardRef<ReactEchartsRef, HeatmapProps>((props, ref) => {
23+
export default function HeatMapChart({
24+
ref,
25+
...props
26+
}: HeatmapProps & {
27+
ref?: React.Ref<ReactEchartsRef>;
28+
}) {
2529
const {series, seriesOptions, visualMaps, ...otherProps} = props;
2630
return (
2731
<BaseChart
@@ -40,4 +44,4 @@ export default forwardRef<ReactEchartsRef, HeatmapProps>((props, ref) => {
4044
)}
4145
/>
4246
);
43-
});
47+
}

0 commit comments

Comments
 (0)