Skip to content

Commit ef1597f

Browse files
Enable react-compiler eslint to spot antipatterns (#28652)
* Switch to React18 useId Signed-off-by: Michael Telatynski <[email protected]> * Enable react-compiler eslint Signed-off-by: Michael Telatynski <[email protected]> * Fix an easy one Signed-off-by: Michael Telatynski <[email protected]> * Disable in tests Signed-off-by: Michael Telatynski <[email protected]> * Fix usage of useRef as memoization Signed-off-by: Michael Telatynski <[email protected]> * Fix mutation of external values in hooks Signed-off-by: Michael Telatynski <[email protected]> * Make React compiler happy about some frankly non-issues Signed-off-by: Michael Telatynski <[email protected]> * Fix MapMock Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> * Revert MemberListViewModel.tsx changes and disable linter per line Signed-off-by: Michael Telatynski <[email protected]> * Make viewmodel compatible with react-compiler linter - Remove searchQuery ref/state and instead pass this query to the loadMember function. - Now we no longer need a separate search function --------- Signed-off-by: Michael Telatynski <[email protected]> Co-authored-by: R Midhun Suresh <[email protected]>
1 parent e5ca795 commit ef1597f

35 files changed

+146
-136
lines changed

.eslintrc.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module.exports = {
2-
plugins: ["matrix-org"],
2+
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
33
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
44
parserOptions: {
55
project: ["./tsconfig.json"],
@@ -170,6 +170,8 @@ module.exports = {
170170
"jsx-a11y/role-supports-aria-props": "off",
171171

172172
"matrix-org/require-copyright-header": "error",
173+
174+
"react-compiler/react-compiler": "error",
173175
},
174176
overrides: [
175177
{
@@ -262,6 +264,7 @@ module.exports = {
262264

263265
// These are fine in tests
264266
"no-restricted-globals": "off",
267+
"react-compiler/react-compiler": "off",
265268
},
266269
},
267270
{

__mocks__/maplibre-gl.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class MockMap extends EventEmitter {
1717
setCenter = jest.fn();
1818
setStyle = jest.fn();
1919
fitBounds = jest.fn();
20+
remove = jest.fn();
2021
}
2122
const MockMapInstance = new MockMap();
2223

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@
237237
"eslint-plugin-jsx-a11y": "^6.5.1",
238238
"eslint-plugin-matrix-org": "^2.0.2",
239239
"eslint-plugin-react": "^7.28.0",
240+
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
240241
"eslint-plugin-react-hooks": "^5.0.0",
241242
"eslint-plugin-unicorn": "^56.0.0",
242243
"express": "^4.18.2",

src/accessibility/RovingTabIndex.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ export const useRovingTabIndex = <T extends HTMLElement>(
392392
});
393393
}, []); // eslint-disable-line react-hooks/exhaustive-deps
394394

395+
// eslint-disable-next-line react-compiler/react-compiler
395396
const isActive = context.state.activeNode === nodeRef.current;
396397
return [onFocus, isActive, ref, nodeRef];
397398
};

src/components/structures/AutocompleteInput.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
142142
{isFocused && suggestions.length ? (
143143
<div
144144
className="mx_AutocompleteInput_matches"
145+
// eslint-disable-next-line react-compiler/react-compiler
145146
style={{ top: editorContainerRef.current?.clientHeight }}
146147
data-testid="autocomplete-matches"
147148
>

src/components/structures/ContextMenu.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ export const useContextMenu = <T extends any = HTMLElement>(inputRef?: RefObject
607607
setIsOpen(false);
608608
};
609609

610+
// eslint-disable-next-line react-compiler/react-compiler
610611
return [button.current ? isOpen : false, button, open, close, setIsOpen];
611612
};
612613

src/components/structures/FilePanel.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,7 @@ class FilePanel extends React.Component<IProps, IState> {
286286
ref={this.card}
287287
header={_t("right_panel|files_button")}
288288
>
289-
{this.card.current && (
290-
<Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />
291-
)}
289+
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
292290
<SearchWarning isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
293291
<TimelinePanel
294292
manageReadReceipts={false}

src/components/structures/NotificationPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
9595
onClose={this.props.onClose}
9696
withoutScrollContainer={true}
9797
>
98-
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
98+
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
9999
{content}
100100
</BaseCard>
101101
</ScopedRoomContextProvider>

src/components/structures/RoomSearchView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
66
Please see LICENSE files in the repository root for full details.
77
*/
88

9-
import React, { forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react";
9+
import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
1010
import {
1111
ISearchResults,
1212
IThreadBundledRelationship,
@@ -58,7 +58,7 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
5858
const [results, setResults] = useState<ISearchResults | null>(null);
5959
const aborted = useRef(false);
6060
// A map from room ID to permalink creator
61-
const permalinkCreators = useRef(new Map<string, RoomPermalinkCreator>()).current;
61+
const permalinkCreators = useMemo(() => new Map<string, RoomPermalinkCreator>(), []);
6262
const innerRef = useRef<ScrollPanel | null>();
6363

6464
useEffect(() => {

src/components/structures/RoomView.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
273273
}
274274

275275
const onRetryClicked = (): void => {
276+
// eslint-disable-next-line react-compiler/react-compiler
276277
room.state = LocalRoomState.NEW;
277278
defaultDispatcher.dispatch({
278279
action: "local_room_event",
@@ -2514,9 +2515,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
25142515
mainSplitContentClassName = "mx_MainSplit_timeline";
25152516
mainSplitBody = (
25162517
<>
2517-
{this.roomViewBody.current && (
2518-
<Measured sensor={this.roomViewBody.current} onMeasurement={this.onMeasurement} />
2519-
)}
2518+
<Measured sensor={this.roomViewBody} onMeasurement={this.onMeasurement} />
25202519
{auxPanel}
25212520
{pinnedMessageBanner}
25222521
<main className={timelineClasses}>

src/components/structures/ThreadPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
204204
ref={card}
205205
closeButtonRef={closeButonRef}
206206
>
207-
{card.current && <Measured sensor={card.current} onMeasurement={setNarrow} />}
207+
<Measured sensor={card} onMeasurement={setNarrow} />
208208
{timelineSet ? (
209209
<TimelinePanel
210210
key={filterOption + ":" + (timelineSet.getFilter()?.filterId ?? roomId)}

src/components/structures/ThreadView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
443443
PosthogTrackers.trackInteraction("WebThreadViewBackButton", ev);
444444
}}
445445
>
446-
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
446+
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
447447
<div className="mx_ThreadView_timelinePanelWrapper">{timeline}</div>
448448

449449
{ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && (

src/components/utils/Box.tsx

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
77
*/
88

99
import classNames from "classnames";
10-
import React, { useEffect, useRef } from "react";
10+
import React, { useMemo } from "react";
1111

1212
type FlexProps = {
1313
/**
@@ -40,25 +40,6 @@ type FlexProps = {
4040
grow?: string | null;
4141
};
4242

43-
/**
44-
* Set or remove a CSS property
45-
* @param ref the reference
46-
* @param name the CSS property name
47-
* @param value the CSS property value
48-
*/
49-
function addOrRemoveProperty(
50-
ref: React.MutableRefObject<HTMLElement | undefined>,
51-
name: string,
52-
value?: string | null,
53-
): void {
54-
const style = ref.current!.style;
55-
if (value) {
56-
style.setProperty(name, value);
57-
} else {
58-
style.removeProperty(name);
59-
}
60-
}
61-
6243
/**
6344
* A flex child helper
6445
*/
@@ -71,12 +52,12 @@ export function Box({
7152
children,
7253
...props
7354
}: React.PropsWithChildren<FlexProps>): JSX.Element {
74-
const ref = useRef<HTMLElement>();
75-
76-
useEffect(() => {
77-
addOrRemoveProperty(ref, `--mx-box-flex`, flex);
78-
addOrRemoveProperty(ref, `--mx-box-shrink`, shrink);
79-
addOrRemoveProperty(ref, `--mx-box-grow`, grow);
55+
const style = useMemo(() => {
56+
const style: Record<string, any> = {};
57+
if (flex) style["--mx-box-flex"] = flex;
58+
if (shrink) style["--mx-box-shrink"] = shrink;
59+
if (grow) style["--mx-box-grow"] = grow;
60+
return style;
8061
}, [flex, grow, shrink]);
8162

8263
return React.createElement(
@@ -88,7 +69,7 @@ export function Box({
8869
"mx_Box--shrink": !!shrink,
8970
"mx_Box--grow": !!grow,
9071
}),
91-
ref,
72+
style,
9273
},
9374
children,
9475
);

src/components/utils/Flex.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
77
*/
88

99
import classNames from "classnames";
10-
import React, { useEffect, useRef } from "react";
10+
import React, { useMemo } from "react";
1111

1212
type FlexProps = {
1313
/**
@@ -64,15 +64,16 @@ export function Flex({
6464
children,
6565
...props
6666
}: React.PropsWithChildren<FlexProps>): JSX.Element {
67-
const ref = useRef<HTMLElement>();
67+
const style = useMemo(
68+
() => ({
69+
"--mx-flex-display": display,
70+
"--mx-flex-direction": direction,
71+
"--mx-flex-align": align,
72+
"--mx-flex-justify": justify,
73+
"--mx-flex-gap": gap,
74+
}),
75+
[align, direction, display, gap, justify],
76+
);
6877

69-
useEffect(() => {
70-
ref.current!.style.setProperty(`--mx-flex-display`, display);
71-
ref.current!.style.setProperty(`--mx-flex-direction`, direction);
72-
ref.current!.style.setProperty(`--mx-flex-align`, align);
73-
ref.current!.style.setProperty(`--mx-flex-justify`, justify);
74-
ref.current!.style.setProperty(`--mx-flex-gap`, gap);
75-
}, [align, direction, display, gap, justify]);
76-
77-
return React.createElement(as, { ...props, className: classNames("mx_Flex", className), ref }, children);
78+
return React.createElement(as, { ...props, className: classNames("mx_Flex", className), style }, children);
7879
}

src/components/viewmodels/memberlist/MemberListViewModel.tsx

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
UserEvent,
2020
} from "matrix-js-sdk/src/matrix";
2121
import { KnownMembership } from "matrix-js-sdk/src/types";
22-
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
22+
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
2323
import { throttle } from "lodash";
2424

2525
import { RoomMember } from "../../../models/rooms/RoomMember";
@@ -120,19 +120,16 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
120120
const sdkContext = useContext(SDKContext);
121121
const [memberMap, setMemberMap] = useState<Map<string, Member>>(new Map());
122122
const [isLoading, setIsLoading] = useState<boolean>(true);
123-
124123
// This is the last known total number of members in this room.
125-
const totalMemberCount = useRef<number>(0);
126-
127-
const searchQuery = useRef("");
124+
const [totalMemberCount, setTotalMemberCount] = useState(0);
128125

129126
const loadMembers = useMemo(
130127
() =>
131128
throttle(
132-
async (): Promise<void> => {
129+
async (searchQuery?: string): Promise<void> => {
133130
const { joined: joinedSdk, invited: invitedSdk } = await sdkContext.memberListStore.loadMemberList(
134131
roomId,
135-
searchQuery.current,
132+
searchQuery,
136133
);
137134
const newMemberMap = new Map<string, Member>();
138135
// First add the invited room members
@@ -141,7 +138,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
141138
newMemberMap.set(member.userId, roomMember);
142139
}
143140
// Then add the third party invites
144-
const threePidInvited = getPending3PidInvites(room, searchQuery.current);
141+
const threePidInvited = getPending3PidInvites(room, searchQuery);
145142
for (const invited of threePidInvited) {
146143
const key = invited.threePidInvite!.event.getContent().display_name;
147144
newMemberMap.set(key, invited);
@@ -152,26 +149,18 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
152149
newMemberMap.set(member.userId, roomMember);
153150
}
154151
setMemberMap(newMemberMap);
155-
if (!searchQuery.current) {
152+
if (!searchQuery) {
156153
/**
157154
* Since searching for members only gives you the relevant
158155
* members matching the query, do not update the totalMemberCount!
159156
**/
160-
totalMemberCount.current = newMemberMap.size;
157+
setTotalMemberCount(newMemberMap.size);
161158
}
162159
},
163160
500,
164161
{ leading: true, trailing: true },
165162
),
166-
[roomId, sdkContext.memberListStore, room],
167-
);
168-
169-
const search = useCallback(
170-
(query: string) => {
171-
searchQuery.current = query;
172-
loadMembers();
173-
},
174-
[loadMembers],
163+
[sdkContext.memberListStore, roomId, room],
175164
);
176165

177166
const isPresenceEnabled = useMemo(
@@ -252,12 +241,12 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
252241

253242
return {
254243
members: Array.from(memberMap.values()),
255-
search,
244+
search: loadMembers,
256245
shouldShowInvite,
257246
isPresenceEnabled,
258247
isLoading,
259248
onInviteButtonClick,
260-
shouldShowSearch: totalMemberCount.current >= 20,
249+
shouldShowSearch: totalMemberCount >= 20,
261250
canInvite,
262251
};
263252
}

src/components/views/elements/EffectsOverlay.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,10 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
5858
if (canvas) canvas.height = UIStore.instance.windowHeight;
5959
UIStore.instance.on(UI_EVENTS.Resize, resize);
6060

61+
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
6162
return () => {
6263
dis.unregister(dispatcherRef);
6364
UIStore.instance.off(UI_EVENTS.Resize, resize);
64-
// eslint-disable-next-line react-hooks/exhaustive-deps
65-
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
6665
for (const effect in currentEffects) {
6766
const effectModule: ICanvasEffect = currentEffects.get(effect)!;
6867
if (effectModule && effectModule.isRunning) {

src/components/views/elements/Measured.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
66
Please see LICENSE files in the repository root for full details.
77
*/
88

9-
import React from "react";
9+
import React, { RefObject } from "react";
1010

1111
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
1212

1313
interface IProps {
14-
sensor: Element;
14+
sensor: RefObject<Element>;
1515
breakpoint: number;
1616
onMeasurement(narrow: boolean): void;
1717
}
@@ -35,14 +35,14 @@ export default class Measured extends React.PureComponent<IProps> {
3535
}
3636

3737
public componentDidUpdate(prevProps: Readonly<IProps>): void {
38-
const previous = prevProps.sensor;
39-
const current = this.props.sensor;
38+
const previous = prevProps.sensor.current;
39+
const current = this.props.sensor.current;
4040
if (previous === current) return;
4141
if (previous) {
4242
UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`);
4343
}
4444
if (current) {
45-
UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, this.props.sensor);
45+
UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, this.props.sensor.current);
4646
}
4747
}
4848

src/components/views/right_panel/TimelineCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
213213
header={_t("right_panel|video_room_chat|title")}
214214
ref={this.card}
215215
>
216-
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
216+
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
217217
<div className="mx_TimelineCard_timeline">
218218
{jumpToBottom}
219219
<TimelinePanel

src/components/views/rooms/ReadReceiptGroup.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export function ReadReceiptGroup({
109109
readReceiptPosition = readReceiptMap[userId];
110110
if (!readReceiptPosition) {
111111
readReceiptPosition = {};
112+
// eslint-disable-next-line react-compiler/react-compiler
112113
readReceiptMap[userId] = readReceiptPosition;
113114
}
114115
}

0 commit comments

Comments
 (0)