diff --git a/.eslintrc.js b/.eslintrc.js index 28d26696cb1..f310384972b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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", }, }, { diff --git a/__mocks__/maplibre-gl.js b/__mocks__/maplibre-gl.js index cac71db330b..475648e7744 100644 --- a/__mocks__/maplibre-gl.js +++ b/__mocks__/maplibre-gl.js @@ -17,6 +17,7 @@ class MockMap extends EventEmitter { setCenter = jest.fn(); setStyle = jest.fn(); fitBounds = jest.fn(); + remove = jest.fn(); } const MockMapInstance = new MockMap(); diff --git a/package.json b/package.json index d79de6e081a..2fb13e438c3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index dada99b3e7f..e2227ea42d2 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -392,6 +392,7 @@ export const useRovingTabIndex = ( }); }, []); // 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]; }; diff --git a/src/components/structures/AutocompleteInput.tsx b/src/components/structures/AutocompleteInput.tsx index b25e93bc75d..25e0d7d1a18 100644 --- a/src/components/structures/AutocompleteInput.tsx +++ b/src/components/structures/AutocompleteInput.tsx @@ -142,6 +142,7 @@ export const AutocompleteInput: React.FC = ({ {isFocused && suggestions.length ? (
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 3d0c1692670..51aef8f454a 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -607,6 +607,7 @@ export const useContextMenu = (inputRef?: RefObject setIsOpen(false); }; + // eslint-disable-next-line react-compiler/react-compiler return [button.current ? isOpen : false, button, open, close, setIsOpen]; }; diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 32e5bbc5194..c1eb34597f8 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -286,9 +286,7 @@ class FilePanel extends React.Component { ref={this.card} header={_t("right_panel|files_button")} > - {this.card.current && ( - - )} + - {this.card.current && } + {content} diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index ca67ca6bbf4..14f34c91462 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -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( const [results, setResults] = useState(null); const aborted = useRef(false); // A map from room ID to permalink creator - const permalinkCreators = useRef(new Map()).current; + const permalinkCreators = useMemo(() => new Map(), []); const innerRef = useRef(); useEffect(() => { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 78d14d39dfb..7f647bfbf3d 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -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 { mainSplitContentClassName = "mx_MainSplit_timeline"; mainSplitBody = ( <> - {this.roomViewBody.current && ( - - )} + {auxPanel} {pinnedMessageBanner}
diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 7aee8554b1d..f6742d81599 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -204,7 +204,7 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => ref={card} closeButtonRef={closeButonRef} > - {card.current && } + {timelineSet ? ( { PosthogTrackers.trackInteraction("WebThreadViewBackButton", ev); }} > - {this.card.current && } +
{timeline}
{ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && ( diff --git a/src/components/utils/Box.tsx b/src/components/utils/Box.tsx index c81c9bafed2..2de64ba0759 100644 --- a/src/components/utils/Box.tsx +++ b/src/components/utils/Box.tsx @@ -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, - 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): JSX.Element { - const ref = useRef(); - - 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 = {}; + 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, ); diff --git a/src/components/utils/Flex.tsx b/src/components/utils/Flex.tsx index ae5704d2479..3788e32c458 100644 --- a/src/components/utils/Flex.tsx +++ b/src/components/utils/Flex.tsx @@ -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): JSX.Element { - const ref = useRef(); + 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); } diff --git a/src/components/viewmodels/memberlist/MemberListViewModel.tsx b/src/components/viewmodels/memberlist/MemberListViewModel.tsx index 4a1a2d59f19..88eacb1b931 100644 --- a/src/components/viewmodels/memberlist/MemberListViewModel.tsx +++ b/src/components/viewmodels/memberlist/MemberListViewModel.tsx @@ -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>(new Map()); const [isLoading, setIsLoading] = useState(true); - // This is the last known total number of members in this room. - const totalMemberCount = useRef(0); - - const searchQuery = useRef(""); + const [totalMemberCount, setTotalMemberCount] = useState(0); const loadMembers = useMemo( () => throttle( - async (): Promise => { + async (searchQuery?: string): Promise => { const { joined: joinedSdk, invited: invitedSdk } = await sdkContext.memberListStore.loadMemberList( roomId, - searchQuery.current, + searchQuery, ); const newMemberMap = new Map(); // 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, }; } diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx index 68733b4ceb1..746a1353902 100644 --- a/src/components/views/elements/EffectsOverlay.tsx +++ b/src/components/views/elements/EffectsOverlay.tsx @@ -58,11 +58,10 @@ const EffectsOverlay: FunctionComponent = ({ 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) { diff --git a/src/components/views/elements/Measured.tsx b/src/components/views/elements/Measured.tsx index 6a4abae2de0..5f0d3acca83 100644 --- a/src/components/views/elements/Measured.tsx +++ b/src/components/views/elements/Measured.tsx @@ -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; breakpoint: number; onMeasurement(narrow: boolean): void; } @@ -35,14 +35,14 @@ export default class Measured extends React.PureComponent { } public componentDidUpdate(prevProps: Readonly): 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); } } diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index 49b313ca445..4cee1ef9b4f 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -213,7 +213,7 @@ export default class TimelineCard extends React.Component { header={_t("right_panel|video_room_chat|title")} ref={this.card} > - {this.card.current && } +
{jumpToBottom} = ({ 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. diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx index 25822b91766..2de986a2996 100644 --- a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx @@ -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 ( - + getDefaultContextValue({ eventRelation: props.eventRelation }), + [props.eventRelation], + ); return ( - + } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts index f5219c6543d..20d877271ef 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts @@ -21,6 +21,7 @@ export function useComposerFunctions( () => ({ clear: () => { if (ref.current) { + // eslint-disable-next-line react-compiler/react-compiler ref.current.innerHTML = ""; } }, diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts index 52613b6b2ae..bc58160ce38 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts @@ -12,6 +12,7 @@ export function usePlainTextInitialization(initialContent = "", ref: RefObject { // 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]); diff --git a/src/components/views/settings/ThemeChoicePanel.tsx b/src/components/views/settings/ThemeChoicePanel.tsx index 0e3926d7e8e..b60b3fe540d 100644 --- a/src/components/views/settings/ThemeChoicePanel.tsx +++ b/src/components/views/settings/ThemeChoicePanel.tsx @@ -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 ( - {themeWatcher.current.isSystemThemeSupported() && ( + {themeWatcher.isSystemThemeSupported() && ( )} diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 2a8425823b0..f7b69696be7 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -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 = ({ 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(); return [ content ?? {}, - content?.["events"] ?? {}, state?.maySendStateEvent(EventType.RoomPowerLevels, room.client.getSafeUserId()), - ]; + ] as const; }, [room.client], ), ); const [elementCallEnabled, setElementCallEnabled] = useState(() => { - 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; diff --git a/src/components/views/spaces/QuickThemeSwitcher.tsx b/src/components/views/spaces/QuickThemeSwitcher.tsx index 1eff342ac39..5d2372647e0 100644 --- a/src/components/views/spaces/QuickThemeSwitcher.tsx +++ b/src/components/views/spaces/QuickThemeSwitcher.tsx @@ -27,7 +27,7 @@ type Props = { const MATCH_SYSTEM_THEME_ID = "MATCH_SYSTEM_THEME_ID"; const QuickThemeSwitcher: React.FC = ({ requestClose }) => { - const orderedThemes = useMemo(getOrderedThemes, []); + const orderedThemes = useMemo(() => getOrderedThemes(), []); const themeState = useTheme(); const nonHighContrast = findNonHighContrastTheme(themeState.theme); diff --git a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts index 94486fdf76e..dea00bafa70 100644 --- a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts +++ b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts @@ -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], ); diff --git a/src/contexts/ScopedRoomContext.tsx b/src/contexts/ScopedRoomContext.tsx index 2b1827952d8..f08911cbb28 100644 --- a/src/contexts/ScopedRoomContext.tsx +++ b/src/contexts/ScopedRoomContext.tsx @@ -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 | 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(state)); + // eslint-disable-next-line react-compiler/react-compiler,react-hooks/exhaustive-deps + const context = useMemo(() => new EfficientContext(state), []); useEffect(() => { - contextRef.current.setState(state); - }, [state]); + context.setState(state); + }, [context, state]); // Includes the legacy RoomContext provider for backwards compatibility with class components return ( - {children} + {children} ); }, diff --git a/src/contexts/ToastContext.tsx b/src/contexts/ToastContext.tsx index 100eb6add09..268539d0e77 100644 --- a/src/contexts/ToastContext.tsx +++ b/src/contexts/ToastContext.tsx @@ -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(toastRack.current.getActiveToast()); + const [activeToast, setActiveToast] = useState(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 { diff --git a/src/hooks/useAsyncRefreshMemo.ts b/src/hooks/useAsyncRefreshMemo.ts index 646217bb63a..3d167d56a6c 100644 --- a/src/hooks/useAsyncRefreshMemo.ts +++ b/src/hooks/useAsyncRefreshMemo.ts @@ -34,7 +34,7 @@ export function useAsyncRefreshMemo(fn: Fn, 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]; } diff --git a/src/hooks/useRoomNotificationState.ts b/src/hooks/useRoomNotificationState.ts index c964501aa9f..e76295a3475 100644 --- a/src/hooks/useRoomNotificationState.ts +++ b/src/hooks/useRoomNotificationState.ts @@ -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]; }; diff --git a/src/hooks/useTransition.ts b/src/hooks/useTransition.ts index e583ba85573..e1f48eeac4a 100644 --- a/src/hooks/useTransition.ts +++ b/src/hooks/useTransition.ts @@ -22,6 +22,6 @@ export const useTransition = (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); }; diff --git a/src/utils/location/useMap.ts b/src/utils/location/useMap.ts index e859559a527..4a299101bd5 100644 --- a/src/utils/location/useMap.ts +++ b/src/utils/location/useMap.ts @@ -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; diff --git a/yarn.lock b/yarn.lock index cbe07be2a19..1322ab213fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"