Enable react-compiler eslint to spot antipatterns (#28652)

* Switch to React18 useId

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Enable react-compiler eslint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix an easy one

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Disable in tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix usage of useRef as memoization

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix mutation of external values in hooks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make React compiler happy about some frankly non-issues

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix MapMock

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Revert MemberListViewModel.tsx changes and disable linter per line

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* 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 <7t3chguy@gmail.com>
Co-authored-by: R Midhun Suresh <hi@midhun.dev>
This commit is contained in:
Michael Telatynski
2025-01-16 12:26:00 +00:00
committed by GitHub
parent e5ca7954c8
commit ef1597ff2d
35 changed files with 146 additions and 136 deletions

View File

@@ -1,5 +1,5 @@
module.exports = {
plugins: ["matrix-org"],
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
parserOptions: {
project: ["./tsconfig.json"],
@@ -170,6 +170,8 @@ module.exports = {
"jsx-a11y/role-supports-aria-props": "off",
"matrix-org/require-copyright-header": "error",
"react-compiler/react-compiler": "error",
},
overrides: [
{
@@ -262,6 +264,7 @@ module.exports = {
// These are fine in tests
"no-restricted-globals": "off",
"react-compiler/react-compiler": "off",
},
},
{

View File

@@ -17,6 +17,7 @@ class MockMap extends EventEmitter {
setCenter = jest.fn();
setStyle = jest.fn();
fitBounds = jest.fn();
remove = jest.fn();
}
const MockMapInstance = new MockMap();

View File

@@ -237,6 +237,7 @@
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "^2.0.2",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-unicorn": "^56.0.0",
"express": "^4.18.2",

View File

@@ -392,6 +392,7 @@ export const useRovingTabIndex = <T extends HTMLElement>(
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-compiler/react-compiler
const isActive = context.state.activeNode === nodeRef.current;
return [onFocus, isActive, ref, nodeRef];
};

View File

@@ -142,6 +142,7 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
{isFocused && suggestions.length ? (
<div
className="mx_AutocompleteInput_matches"
// eslint-disable-next-line react-compiler/react-compiler
style={{ top: editorContainerRef.current?.clientHeight }}
data-testid="autocomplete-matches"
>

View File

@@ -607,6 +607,7 @@ export const useContextMenu = <T extends any = HTMLElement>(inputRef?: RefObject
setIsOpen(false);
};
// eslint-disable-next-line react-compiler/react-compiler
return [button.current ? isOpen : false, button, open, close, setIsOpen];
};

View File

@@ -286,9 +286,7 @@ class FilePanel extends React.Component<IProps, IState> {
ref={this.card}
header={_t("right_panel|files_button")}
>
{this.card.current && (
<Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />
)}
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
<SearchWarning isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
<TimelinePanel
manageReadReceipts={false}

View File

@@ -95,7 +95,7 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
onClose={this.props.onClose}
withoutScrollContainer={true}
>
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
{content}
</BaseCard>
</ScopedRoomContextProvider>

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react";
import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import {
ISearchResults,
IThreadBundledRelationship,
@@ -58,7 +58,7 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
const [results, setResults] = useState<ISearchResults | null>(null);
const aborted = useRef(false);
// A map from room ID to permalink creator
const permalinkCreators = useRef(new Map<string, RoomPermalinkCreator>()).current;
const permalinkCreators = useMemo(() => new Map<string, RoomPermalinkCreator>(), []);
const innerRef = useRef<ScrollPanel | null>();
useEffect(() => {

View File

@@ -273,6 +273,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
}
const onRetryClicked = (): void => {
// eslint-disable-next-line react-compiler/react-compiler
room.state = LocalRoomState.NEW;
defaultDispatcher.dispatch({
action: "local_room_event",
@@ -2514,9 +2515,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
mainSplitContentClassName = "mx_MainSplit_timeline";
mainSplitBody = (
<>
{this.roomViewBody.current && (
<Measured sensor={this.roomViewBody.current} onMeasurement={this.onMeasurement} />
)}
<Measured sensor={this.roomViewBody} onMeasurement={this.onMeasurement} />
{auxPanel}
{pinnedMessageBanner}
<main className={timelineClasses}>

View File

@@ -204,7 +204,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
ref={card}
closeButtonRef={closeButonRef}
>
{card.current && <Measured sensor={card.current} onMeasurement={setNarrow} />}
<Measured sensor={card} onMeasurement={setNarrow} />
{timelineSet ? (
<TimelinePanel
key={filterOption + ":" + (timelineSet.getFilter()?.filterId ?? roomId)}

View File

@@ -443,7 +443,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
PosthogTrackers.trackInteraction("WebThreadViewBackButton", ev);
}}
>
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
<div className="mx_ThreadView_timelinePanelWrapper">{timeline}</div>
{ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && (

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { useEffect, useRef } from "react";
import React, { useMemo } from "react";
type FlexProps = {
/**
@@ -40,25 +40,6 @@ type FlexProps = {
grow?: string | null;
};
/**
* Set or remove a CSS property
* @param ref the reference
* @param name the CSS property name
* @param value the CSS property value
*/
function addOrRemoveProperty(
ref: React.MutableRefObject<HTMLElement | undefined>,
name: string,
value?: string | null,
): void {
const style = ref.current!.style;
if (value) {
style.setProperty(name, value);
} else {
style.removeProperty(name);
}
}
/**
* A flex child helper
*/
@@ -71,12 +52,12 @@ export function Box({
children,
...props
}: React.PropsWithChildren<FlexProps>): JSX.Element {
const ref = useRef<HTMLElement>();
useEffect(() => {
addOrRemoveProperty(ref, `--mx-box-flex`, flex);
addOrRemoveProperty(ref, `--mx-box-shrink`, shrink);
addOrRemoveProperty(ref, `--mx-box-grow`, grow);
const style = useMemo(() => {
const style: Record<string, any> = {};
if (flex) style["--mx-box-flex"] = flex;
if (shrink) style["--mx-box-shrink"] = shrink;
if (grow) style["--mx-box-grow"] = grow;
return style;
}, [flex, grow, shrink]);
return React.createElement(
@@ -88,7 +69,7 @@ export function Box({
"mx_Box--shrink": !!shrink,
"mx_Box--grow": !!grow,
}),
ref,
style,
},
children,
);

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { useEffect, useRef } from "react";
import React, { useMemo } from "react";
type FlexProps = {
/**
@@ -64,15 +64,16 @@ export function Flex({
children,
...props
}: React.PropsWithChildren<FlexProps>): JSX.Element {
const ref = useRef<HTMLElement>();
const style = useMemo(
() => ({
"--mx-flex-display": display,
"--mx-flex-direction": direction,
"--mx-flex-align": align,
"--mx-flex-justify": justify,
"--mx-flex-gap": gap,
}),
[align, direction, display, gap, justify],
);
useEffect(() => {
ref.current!.style.setProperty(`--mx-flex-display`, display);
ref.current!.style.setProperty(`--mx-flex-direction`, direction);
ref.current!.style.setProperty(`--mx-flex-align`, align);
ref.current!.style.setProperty(`--mx-flex-justify`, justify);
ref.current!.style.setProperty(`--mx-flex-gap`, gap);
}, [align, direction, display, gap, justify]);
return React.createElement(as, { ...props, className: classNames("mx_Flex", className), ref }, children);
return React.createElement(as, { ...props, className: classNames("mx_Flex", className), style }, children);
}

View File

@@ -19,7 +19,7 @@ import {
UserEvent,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { throttle } from "lodash";
import { RoomMember } from "../../../models/rooms/RoomMember";
@@ -120,19 +120,16 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
const sdkContext = useContext(SDKContext);
const [memberMap, setMemberMap] = useState<Map<string, Member>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(true);
// This is the last known total number of members in this room.
const totalMemberCount = useRef<number>(0);
const searchQuery = useRef("");
const [totalMemberCount, setTotalMemberCount] = useState(0);
const loadMembers = useMemo(
() =>
throttle(
async (): Promise<void> => {
async (searchQuery?: string): Promise<void> => {
const { joined: joinedSdk, invited: invitedSdk } = await sdkContext.memberListStore.loadMemberList(
roomId,
searchQuery.current,
searchQuery,
);
const newMemberMap = new Map<string, Member>();
// First add the invited room members
@@ -141,7 +138,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
newMemberMap.set(member.userId, roomMember);
}
// Then add the third party invites
const threePidInvited = getPending3PidInvites(room, searchQuery.current);
const threePidInvited = getPending3PidInvites(room, searchQuery);
for (const invited of threePidInvited) {
const key = invited.threePidInvite!.event.getContent().display_name;
newMemberMap.set(key, invited);
@@ -152,26 +149,18 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
newMemberMap.set(member.userId, roomMember);
}
setMemberMap(newMemberMap);
if (!searchQuery.current) {
if (!searchQuery) {
/**
* Since searching for members only gives you the relevant
* members matching the query, do not update the totalMemberCount!
**/
totalMemberCount.current = newMemberMap.size;
setTotalMemberCount(newMemberMap.size);
}
},
500,
{ leading: true, trailing: true },
),
[roomId, sdkContext.memberListStore, room],
);
const search = useCallback(
(query: string) => {
searchQuery.current = query;
loadMembers();
},
[loadMembers],
[sdkContext.memberListStore, roomId, room],
);
const isPresenceEnabled = useMemo(
@@ -252,12 +241,12 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
return {
members: Array.from(memberMap.values()),
search,
search: loadMembers,
shouldShowInvite,
isPresenceEnabled,
isLoading,
onInviteButtonClick,
shouldShowSearch: totalMemberCount.current >= 20,
shouldShowSearch: totalMemberCount >= 20,
canInvite,
};
}

View File

@@ -58,11 +58,10 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
if (canvas) canvas.height = UIStore.instance.windowHeight;
UIStore.instance.on(UI_EVENTS.Resize, resize);
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
return () => {
dis.unregister(dispatcherRef);
UIStore.instance.off(UI_EVENTS.Resize, resize);
// eslint-disable-next-line react-hooks/exhaustive-deps
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
for (const effect in currentEffects) {
const effectModule: ICanvasEffect = currentEffects.get(effect)!;
if (effectModule && effectModule.isRunning) {

View File

@@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { RefObject } from "react";
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
interface IProps {
sensor: Element;
sensor: RefObject<Element>;
breakpoint: number;
onMeasurement(narrow: boolean): void;
}
@@ -35,14 +35,14 @@ export default class Measured extends React.PureComponent<IProps> {
}
public componentDidUpdate(prevProps: Readonly<IProps>): void {
const previous = prevProps.sensor;
const current = this.props.sensor;
const previous = prevProps.sensor.current;
const current = this.props.sensor.current;
if (previous === current) return;
if (previous) {
UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`);
}
if (current) {
UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, this.props.sensor);
UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, this.props.sensor.current);
}
}

View File

@@ -213,7 +213,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
header={_t("right_panel|video_room_chat|title")}
ref={this.card}
>
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
<div className="mx_TimelineCard_timeline">
{jumpToBottom}
<TimelinePanel

View File

@@ -109,6 +109,7 @@ export function ReadReceiptGroup({
readReceiptPosition = readReceiptMap[userId];
if (!readReceiptPosition) {
readReceiptPosition = {};
// eslint-disable-next-line react-compiler/react-compiler
readReceiptMap[userId] = readReceiptPosition;
}
}

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { useCallback, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { EventType, KnownMembership, MatrixEvent, Room, RoomStateEvent, RoomMember } from "matrix-js-sdk/src/matrix";
import { CryptoApi, CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger";
@@ -213,9 +213,11 @@ export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }
initialisedRef.current = InitialisationStatus.Completed;
}, [crypto, room, addMembersWhoNeedApproval, updateCurrentPrompt]);
loadMembers().catch((e) => {
logger.error("Error initialising UserIdentityWarning:", e);
});
useEffect(() => {
loadMembers().catch((e) => {
logger.error("Error initialising UserIdentityWarning:", e);
});
}, [loadMembers]);
// When a user's verification status changes, we check if they need to be
// added/removed from the set of members needing approval.

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react";
import React, { ForwardedRef, forwardRef, MutableRefObject, useMemo } from "react";
import classNames from "classnames";
import EditorStateTransfer from "../../../../utils/EditorStateTransfer";
@@ -44,7 +44,7 @@ export default function EditWysiwygComposer({
className,
...props
}: EditWysiwygComposerProps): JSX.Element {
const defaultContextValue = useRef(getDefaultContextValue({ editorStateTransfer }));
const defaultContextValue = useMemo(() => getDefaultContextValue({ editorStateTransfer }), [editorStateTransfer]);
const initialContent = useInitialContent(editorStateTransfer);
const isReady = !editorStateTransfer || initialContent !== undefined;
@@ -55,7 +55,7 @@ export default function EditWysiwygComposer({
}
return (
<ComposerContext.Provider value={defaultContextValue.current}>
<ComposerContext.Provider value={defaultContextValue}>
<WysiwygComposer
className={classNames("mx_EditWysiwygComposer", className)}
initialContent={initialContent}

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react";
import React, { ForwardedRef, forwardRef, MutableRefObject, useMemo } from "react";
import { IEventRelation } from "matrix-js-sdk/src/matrix";
import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler";
@@ -52,10 +52,13 @@ export default function SendWysiwygComposer({
...props
}: SendWysiwygComposerProps): JSX.Element {
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
const defaultContextValue = useRef(getDefaultContextValue({ eventRelation: props.eventRelation }));
const defaultContextValue = useMemo(
() => getDefaultContextValue({ eventRelation: props.eventRelation }),
[props.eventRelation],
);
return (
<ComposerContext.Provider value={defaultContextValue.current}>
<ComposerContext.Provider value={defaultContextValue}>
<Composer
className="mx_SendWysiwygComposer"
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />}

View File

@@ -21,6 +21,7 @@ export function useComposerFunctions(
() => ({
clear: () => {
if (ref.current) {
// eslint-disable-next-line react-compiler/react-compiler
ref.current.innerHTML = "";
}
},

View File

@@ -12,6 +12,7 @@ export function usePlainTextInitialization(initialContent = "", ref: RefObject<H
useEffect(() => {
// always read and write the ref.current using .innerHTML for consistency in linebreak and HTML entity handling
if (ref.current) {
// eslint-disable-next-line react-compiler/react-compiler
ref.current.innerHTML = initialContent;
}
}, [ref, initialContent]);

View File

@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { ChangeEvent, JSX, useCallback, useMemo, useRef, useState } from "react";
import React, { ChangeEvent, JSX, useCallback, useMemo, useState } from "react";
import {
InlineField,
ToggleControl,
@@ -39,12 +39,12 @@ import { useSettingValue } from "../../../hooks/useSettings";
*/
export function ThemeChoicePanel(): JSX.Element {
const themeState = useTheme();
const themeWatcher = useRef(new ThemeWatcher());
const themeWatcher = useMemo(() => new ThemeWatcher(), []);
const customThemeEnabled = useSettingValue("feature_custom_themes");
return (
<SettingsSubsection heading={_t("common|theme")} legacy={false} data-testid="themePanel">
{themeWatcher.current.isSystemThemeSupported() && (
{themeWatcher.isSystemThemeSupported() && (
<SystemTheme systemThemeActivated={themeState.systemThemeActivated} />
)}
<ThemeSelectors theme={themeState.theme} disabled={themeState.systemThemeActivated} />

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { useCallback, useMemo, useState } from "react";
import { JoinRule, EventType, RoomState, Room } from "matrix-js-sdk/src/matrix";
import { RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types";
import { _t } from "../../../../../languageHandler";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
@@ -24,48 +25,49 @@ interface ElementCallSwitchProps {
const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ room }) => {
const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]);
const [content, events, maySend] = useRoomState(
const [content, maySend] = useRoomState(
room,
useCallback(
(state: RoomState) => {
const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
const content = state
?.getStateEvents(EventType.RoomPowerLevels, "")
?.getContent<RoomPowerLevelsEventContent>();
return [
content ?? {},
content?.["events"] ?? {},
state?.maySendStateEvent(EventType.RoomPowerLevels, room.client.getSafeUserId()),
];
] as const;
},
[room.client],
),
);
const [elementCallEnabled, setElementCallEnabled] = useState<boolean>(() => {
return events[ElementCall.MEMBER_EVENT_TYPE.name] === 0;
return content.events?.[ElementCall.MEMBER_EVENT_TYPE.name] === 0;
});
const onChange = useCallback(
(enabled: boolean): void => {
setElementCallEnabled(enabled);
// Take a copy to avoid mutating the original
const newContent = { events: {}, ...content };
if (enabled) {
const userLevel = events[EventType.RoomMessage] ?? content.users_default ?? 0;
const userLevel = newContent.events[EventType.RoomMessage] ?? content.users_default ?? 0;
const moderatorLevel = content.kick ?? 50;
events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel;
events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel;
newContent.events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel;
newContent.events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel;
} else {
const adminLevel = events[EventType.RoomPowerLevels] ?? content.state_default ?? 100;
const adminLevel = newContent.events[EventType.RoomPowerLevels] ?? content.state_default ?? 100;
events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel;
events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel;
newContent.events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel;
newContent.events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel;
}
room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, {
events: events,
...content,
});
room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent);
},
[room.client, room.roomId, content, events, isPublic],
[room.client, room.roomId, content, isPublic],
);
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;

View File

@@ -27,7 +27,7 @@ type Props = {
const MATCH_SYSTEM_THEME_ID = "MATCH_SYSTEM_THEME_ID";
const QuickThemeSwitcher: React.FC<Props> = ({ requestClose }) => {
const orderedThemes = useMemo(getOrderedThemes, []);
const orderedThemes = useMemo(() => getOrderedThemes(), []);
const themeState = useTheme();
const nonHighContrast = findNonHighContrastTheme(themeState.theme);

View File

@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { ClientEvent, MatrixClient, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix";
import { throttle } from "lodash";
@@ -42,14 +42,12 @@ export function useUnreadThreadRooms(forceComputation: boolean): Result {
setResult(computeUnreadThreadRooms(mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs));
}, [mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs]);
// The exhautive deps lint rule can't compute dependencies here since it's not a plain inline func.
// We make this as simple as possible so its only dep is doUpdate itself.
// eslint-disable-next-line react-hooks/exhaustive-deps
const scheduleUpdate = useCallback(
throttle(doUpdate, MIN_UPDATE_INTERVAL_MS, {
leading: false,
trailing: true,
}),
const scheduleUpdate = useMemo(
() =>
throttle(doUpdate, MIN_UPDATE_INTERVAL_MS, {
leading: false,
trailing: true,
}),
[doUpdate],
);

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
import React, { ContextType, createContext, memo, ReactNode, useContext, useEffect, useRef, useState } from "react";
import React, { ContextType, createContext, memo, ReactNode, useContext, useEffect, useMemo, useState } from "react";
import { objectKeyChanges } from "../utils/objects.ts";
import { useTypedEventEmitter } from "../hooks/useEventEmitter.ts";
@@ -48,15 +48,16 @@ const ScopedRoomContext = createContext<EfficientContext<ContextValue> | undefin
// Uses react memo and leverages splatting the value to ensure that the context is only updated when the state changes (shallow compare)
export const ScopedRoomContextProvider = memo(
({ children, ...state }: { children: ReactNode } & ContextValue): JSX.Element => {
const contextRef = useRef(new EfficientContext<ContextValue>(state));
// eslint-disable-next-line react-compiler/react-compiler,react-hooks/exhaustive-deps
const context = useMemo(() => new EfficientContext<ContextValue>(state), []);
useEffect(() => {
contextRef.current.setState(state);
}, [state]);
context.setState(state);
}, [context, state]);
// Includes the legacy RoomContext provider for backwards compatibility with class components
return (
<RoomContext.Provider value={state}>
<ScopedRoomContext.Provider value={contextRef.current}>{children}</ScopedRoomContext.Provider>
<ScopedRoomContext.Provider value={context}>{children}</ScopedRoomContext.Provider>
</RoomContext.Provider>
);
},

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
import { ReactNode, createContext, useCallback, useContext, useEffect, useState, useMemo } from "react";
/**
* A ToastContext helps components display any kind of toast message and can be provided
@@ -33,19 +33,19 @@ export function useToastContext(): ToastRack {
* the ToastRack object that should be provided to the context
*/
export function useActiveToast(): [ReactNode | undefined, ToastRack] {
const toastRack = useRef(new ToastRack());
const toastRack = useMemo(() => new ToastRack(), []);
const [activeToast, setActiveToast] = useState<ReactNode | undefined>(toastRack.current.getActiveToast());
const [activeToast, setActiveToast] = useState<ReactNode | undefined>(toastRack.getActiveToast());
const updateCallback = useCallback(() => {
setActiveToast(toastRack.current.getActiveToast());
setActiveToast(toastRack.getActiveToast());
}, [setActiveToast, toastRack]);
useEffect(() => {
toastRack.current.setCallback(updateCallback);
toastRack.setCallback(updateCallback);
}, [toastRack, updateCallback]);
return [activeToast, toastRack.current];
return [activeToast, toastRack];
}
interface DisplayedToast {

View File

@@ -34,7 +34,7 @@ export function useAsyncRefreshMemo<T>(fn: Fn<T>, deps: DependencyList, initialV
return () => {
discard = true;
};
}, deps); // eslint-disable-line react-hooks/exhaustive-deps
}, deps); // eslint-disable-line react-hooks/exhaustive-deps,react-compiler/react-compiler
useEffect(refresh, [refresh]);
return [value, refresh];
}

View File

@@ -25,6 +25,7 @@ export const useNotificationState = (room: Room): [RoomNotifState | undefined, (
setNotificationState(echoChamber.notificationVolume);
}
});
// eslint-disable-next-line react-compiler/react-compiler
const setter = useCallback((state: RoomNotifState) => (echoChamber.notificationVolume = state), [echoChamber]);
return [notificationState, setter];
};

View File

@@ -22,6 +22,6 @@ export const useTransition = <D extends DependencyList>(callback: (...params: D)
useEffect(() => {
if (args.current !== null) func.current(...args.current);
args.current = deps;
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-compiler/react-compiler,react-hooks/exhaustive-deps
}, deps);
};

View File

@@ -30,8 +30,10 @@ export const useMap = ({ interactive, bodyId, onError }: UseMapProps): MapLibreM
useEffect(
() => {
let map: MapLibreMap | undefined;
try {
setMap(createMap(cli, !!interactive, bodyId, onError));
map = createMap(cli, !!interactive, bodyId, onError);
setMap(map);
} catch (error) {
console.error("Error encountered in useMap", error);
if (error instanceof Error) {
@@ -46,8 +48,7 @@ export const useMap = ({ interactive, bodyId, onError }: UseMapProps): MapLibreM
};
},
// map is excluded as a dependency
// eslint-disable-next-line react-hooks/exhaustive-deps
[interactive, bodyId, onError],
[cli, interactive, bodyId, onError],
);
return map;

View File

@@ -61,7 +61,7 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02"
integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==
"@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.18.5", "@babel/core@^7.21.3", "@babel/core@^7.23.9":
"@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.18.5", "@babel/core@^7.21.3", "@babel/core@^7.23.9", "@babel/core@^7.24.4":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40"
integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==
@@ -303,7 +303,7 @@
dependencies:
"@babel/types" "^7.25.8"
"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3":
"@babel/parser@^7.24.4", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3":
version "7.26.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234"
integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==
@@ -5977,6 +5977,18 @@ eslint-plugin-matrix-org@^2.0.2:
resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-2.0.2.tgz#95b86b0f16704ab19740f7c3c62eae69e20365e6"
integrity sha512-cQy5Rjeq6uyu1mLXlPZwEJdyM0NmclrnEz68y792FSuuxzMyJNNYLGDQ5CkYW8H+PrD825HUFZ34pNXnjMOzOw==
eslint-plugin-react-compiler@^19.0.0-beta-df7b47d-20241124:
version "19.0.0-beta-df7b47d-20241124"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.0.0-beta-df7b47d-20241124.tgz#468751d3a8a6781189405ee56b39b80545306df8"
integrity sha512-82PfnllC8jP/68KdLAbpWuYTcfmtGLzkqy2IW85WopKMTr+4rdQpp+lfliQ/QE79wWrv/dRoADrk3Pdhq25nTw==
dependencies:
"@babel/core" "^7.24.4"
"@babel/parser" "^7.24.4"
"@babel/plugin-transform-private-methods" "^7.25.9"
hermes-parser "^0.25.1"
zod "^3.22.4"
zod-validation-error "^3.0.3"
eslint-plugin-react-hooks@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz#72e2eefbac4b694f5324154619fee44f5f60f101"
@@ -6936,6 +6948,18 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hermes-estree@0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
hermes-parser@^0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
dependencies:
hermes-estree "0.25.1"
highlight.js@^11.3.1:
version "11.10.0"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92"