diff --git a/package-lock.json b/package-lock.json
index 36c94fb58..218968584 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -105,6 +105,7 @@
"@vitejs/plugin-react": "^4.5.0",
"@vitest/coverage-v8": "^3.2.3",
"@vitest/eslint-plugin": "^1.3.4",
+ "babel-plugin-react-compiler": "^19.1.0-rc.2",
"emojibase-data": "^16.0.3",
"eslint": "^9.29.0",
"eslint-plugin-compat": "^6.0.2",
@@ -3833,6 +3834,16 @@
"npm": ">=6"
}
},
+ "node_modules/babel-plugin-react-compiler": {
+ "version": "19.1.0-rc.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.2.tgz",
+ "integrity": "sha512-kSNA//p5fMO6ypG8EkEVPIqAjwIXm5tMjfD1XRPL/sRjYSbJ6UsvORfaeolNWnZ9n310aM0xJP7peW26BuCVzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.26.0"
+ }
+ },
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
diff --git a/package.json b/package.json
index 0b592bb67..77c527f9b 100644
--- a/package.json
+++ b/package.json
@@ -101,6 +101,7 @@
"@vitejs/plugin-react": "^4.5.0",
"@vitest/coverage-v8": "^3.2.3",
"@vitest/eslint-plugin": "^1.3.4",
+ "babel-plugin-react-compiler": "^19.1.0-rc.2",
"emojibase-data": "^16.0.3",
"eslint": "^9.29.0",
"eslint-plugin-compat": "^6.0.2",
diff --git a/src/components/Chat/ChatMessages.tsx b/src/components/Chat/ChatMessages.tsx
index a12ff9e81..e52953f4e 100644
--- a/src/components/Chat/ChatMessages.tsx
+++ b/src/components/Chat/ChatMessages.tsx
@@ -72,13 +72,13 @@ function ChatMessages({
// Scroll to bottom again if the last message changes.
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : undefined;
+ // Use a ref to avoid triggering the effect repeatedly when scrolling _up_ from the bottom.
+ const scrolledToBottomRef = useRef(isScrolledToBottom);
+ scrolledToBottomRef.current = isScrolledToBottom;
useEffect(() => {
- if (isScrolledToBottom && container.current) {
+ if (scrolledToBottomRef.current && container.current) {
scrollToBottom(container.current);
}
- // We need to scroll to the bottom only if a new message comes in, not when the scroll-to-bottom
- // state changed.
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastMessage]);
// Accept externally controlled scrolling using the global event bus, so the chat input box
diff --git a/src/components/Chat/LogMessage.tsx b/src/components/Chat/LogMessage.tsx
index d4ec33c75..6f76c9f71 100644
--- a/src/components/Chat/LogMessage.tsx
+++ b/src/components/Chat/LogMessage.tsx
@@ -1,5 +1,3 @@
-import { memo } from 'react';
-
type LogMessageProps = {
text: string,
};
@@ -13,4 +11,4 @@ function LogMessage({ text }: LogMessageProps) {
);
}
-export default memo(LogMessage);
+export default LogMessage;
diff --git a/src/components/Chat/Message.tsx b/src/components/Chat/Message.tsx
index c8202eed6..007e04fa5 100644
--- a/src/components/Chat/Message.tsx
+++ b/src/components/Chat/Message.tsx
@@ -1,7 +1,5 @@
import cx from 'clsx';
-import {
- memo, useCallback, useMemo, useRef,
-} from 'react';
+import { useCallback, useMemo, useRef } from 'react';
import type { MarkupNode } from 'u-wave-parse-chat-markup';
import type { User } from '../../reducers/users';
import useUserCard from '../../hooks/useUserCard';
@@ -29,7 +27,7 @@ function DeleteButton({ onDelete }: DeleteButtonProps) {
type MessageTimestampProps = {
date: Date,
};
-function MessageTimestampImpl({ date }: MessageTimestampProps) {
+function MessageTimestamp({ date }: MessageTimestampProps) {
const { timeFormatter } = useIntl();
return (
@@ -41,7 +39,6 @@ function MessageTimestampImpl({ date }: MessageTimestampProps) {
);
}
-const MessageTimestamp = memo(MessageTimestampImpl);
type ChatMessageProps = {
_id: string,
diff --git a/src/components/Chat/Motd.tsx b/src/components/Chat/Motd.tsx
index 5f4a0c180..6ada27086 100644
--- a/src/components/Chat/Motd.tsx
+++ b/src/components/Chat/Motd.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import type { MarkupNode } from 'u-wave-parse-chat-markup';
import Markup, { type CompileOptions } from './Markup';
@@ -16,4 +15,4 @@ function Motd({ children, compileOptions }: MotdProps) {
);
}
-export default React.memo(Motd);
+export default Motd;
diff --git a/src/components/Dialogs/LoginDialog/SocialLogin.tsx b/src/components/Dialogs/LoginDialog/SocialLogin.tsx
index 6b72593c2..81db09001 100644
--- a/src/components/Dialogs/LoginDialog/SocialLogin.tsx
+++ b/src/components/Dialogs/LoginDialog/SocialLogin.tsx
@@ -1,11 +1,9 @@
-import React from 'react';
+import { lazy, Suspense } from 'react';
import { useTranslator } from '@u-wave/react-translate';
import { useDispatch } from '../../../hooks/useRedux';
import { loginWithGoogle } from '../../../actions/LoginActionCreators';
-const GoogleButton = React.lazy(() => (
- import('react-google-button')
-));
+const GoogleButton = lazy(() => import('react-google-button'));
const loadingGoogleButton =
;
function SocialLogin() {
@@ -13,14 +11,14 @@ function SocialLogin() {
const dispatch = useDispatch();
return (
-
+
dispatch(loginWithGoogle())}
/>
-
+
);
}
-export default React.memo(SocialLogin);
+export default SocialLogin;
diff --git a/src/components/FooterBar/SettingsButton.tsx b/src/components/FooterBar/SettingsButton.tsx
index f1398ff6e..581819797 100644
--- a/src/components/FooterBar/SettingsButton.tsx
+++ b/src/components/FooterBar/SettingsButton.tsx
@@ -1,4 +1,3 @@
-import { memo } from 'react';
import { useTranslator } from '@u-wave/react-translate';
import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
@@ -22,4 +21,4 @@ function SettingsButton({ onClick }: SettingsButtonProps) {
);
}
-export default memo(SettingsButton);
+export default SettingsButton;
diff --git a/src/components/FooterBar/SkipButton.tsx b/src/components/FooterBar/SkipButton.tsx
index 09ccfd5d3..bc81546e5 100644
--- a/src/components/FooterBar/SkipButton.tsx
+++ b/src/components/FooterBar/SkipButton.tsx
@@ -1,5 +1,4 @@
import {
- memo,
useCallback,
useRef,
useState,
@@ -107,4 +106,4 @@ function SkipButton({ userIsDJ, currentDJ, onSkip }: SkipButtonProps) {
);
}
-export default memo(SkipButton);
+export default SkipButton;
diff --git a/src/components/FooterBar/UserInfo.tsx b/src/components/FooterBar/UserInfo.tsx
index cc3470dc3..cbcc8140f 100644
--- a/src/components/FooterBar/UserInfo.tsx
+++ b/src/components/FooterBar/UserInfo.tsx
@@ -1,5 +1,4 @@
import cx from 'clsx';
-import { memo } from 'react';
import { mdiCog } from '@mdi/js';
import Avatar from '../Avatar';
import SvgIcon from '../SvgIcon';
@@ -28,4 +27,4 @@ function UserInfo({ className, user, onClick }: UserInfoProps) {
);
}
-export default memo(UserInfo);
+export default UserInfo;
diff --git a/src/components/HeaderBar/HistoryButton.tsx b/src/components/HeaderBar/HistoryButton.tsx
index 123d75c42..49350698a 100644
--- a/src/components/HeaderBar/HistoryButton.tsx
+++ b/src/components/HeaderBar/HistoryButton.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import { useTranslator } from '@u-wave/react-translate';
import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
@@ -25,4 +24,4 @@ function HistoryButton({ onClick }: HistoryButtonProps) {
);
}
-export default React.memo(HistoryButton);
+export default HistoryButton;
diff --git a/src/components/MediaList/MediaSourceIcon.tsx b/src/components/MediaList/MediaSourceIcon.tsx
index 7466f3fca..7ba25e7b1 100644
--- a/src/components/MediaList/MediaSourceIcon.tsx
+++ b/src/components/MediaList/MediaSourceIcon.tsx
@@ -1,4 +1,3 @@
-import { memo } from 'react';
import { useMediaSources } from '../../context/MediaSourceContext';
type MediaSourceIconProps = {
@@ -22,4 +21,4 @@ function MediaSourceIcon({ sourceType }: MediaSourceIconProps) {
);
}
-export default memo(MediaSourceIcon);
+export default MediaSourceIcon;
diff --git a/src/components/MediaList/MediaThumbnail.tsx b/src/components/MediaList/MediaThumbnail.tsx
index 8e9eed572..668a43e56 100644
--- a/src/components/MediaList/MediaThumbnail.tsx
+++ b/src/components/MediaList/MediaThumbnail.tsx
@@ -1,5 +1,3 @@
-import { memo } from 'react';
-
type MediaThumbnailProps = {
url: string,
};
@@ -16,4 +14,4 @@ function MediaThumbnail({ url }: MediaThumbnailProps) {
);
}
-export default memo(MediaThumbnail);
+export default MediaThumbnail;
diff --git a/src/components/MediaList/Row.tsx b/src/components/MediaList/Row.tsx
index b099ad9ec..84beda8ec 100644
--- a/src/components/MediaList/Row.tsx
+++ b/src/components/MediaList/Row.tsx
@@ -1,5 +1,4 @@
import cx from 'clsx';
-import React from 'react';
import MediaRowBase from './MediaRowBase';
import MediaDuration from './MediaDuration';
import MediaLoadingIndicator from './MediaLoadingIndicator';
@@ -56,4 +55,4 @@ function MediaRow({
);
}
-export default React.memo(MediaRow);
+export default MediaRow;
diff --git a/src/components/SearchBar/index.tsx b/src/components/SearchBar/index.tsx
index 0b44a8107..345fdd049 100644
--- a/src/components/SearchBar/index.tsx
+++ b/src/components/SearchBar/index.tsx
@@ -30,12 +30,11 @@ function SearchBar({
}
}, [onSubmit]);
+ const initialAutoFocus = useRef(autoFocus ?? false);
useEffect(() => {
- if (autoFocus && inputRef.current) {
+ if (initialAutoFocus.current && inputRef.current != null) {
inputRef.current.focus();
}
- // `autoFocus` is only checked on mount on purpose.
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
diff --git a/src/components/SongTitle/index.tsx b/src/components/SongTitle/index.tsx
index 7d41bbcd7..f18250053 100644
--- a/src/components/SongTitle/index.tsx
+++ b/src/components/SongTitle/index.tsx
@@ -1,5 +1,4 @@
import cx from 'clsx';
-import React from 'react';
type SongTitleProps = {
className?: string,
@@ -29,4 +28,4 @@ function SongTitle({
);
}
-export default React.memo(SongTitle);
+export default SongTitle;
diff --git a/src/components/SvgIcon/index.tsx b/src/components/SvgIcon/index.tsx
index ac30e86ff..0433b2082 100644
--- a/src/components/SvgIcon/index.tsx
+++ b/src/components/SvgIcon/index.tsx
@@ -1,5 +1,4 @@
import cx from 'clsx';
-import React from 'react';
export interface SvgIconProps extends Omit, 'ref'> {
path?: string;
@@ -18,4 +17,4 @@ function SvgIcon({
);
}
-export default React.memo(SvgIcon);
+export default SvgIcon;
diff --git a/src/components/Username/index.tsx b/src/components/Username/index.tsx
index 1fc4fbe66..ac0e5ed5a 100644
--- a/src/components/Username/index.tsx
+++ b/src/components/Username/index.tsx
@@ -1,5 +1,4 @@
import cx from 'clsx';
-import React from 'react';
import RoleColor from '../RoleColor';
type UsernameProps = {
@@ -18,4 +17,4 @@ function Username({ className, user }: UsernameProps) {
);
}
-export default React.memo(Username);
+export default Username;
diff --git a/vite.config.mjs b/vite.config.mjs
index 059cafa4a..7809a9155 100644
--- a/vite.config.mjs
+++ b/vite.config.mjs
@@ -38,7 +38,13 @@ export default defineConfig({
},
},
plugins: [
- react(),
+ react({
+ babel: {
+ plugins: [
+ ['react-compiler', { target: '19' }],
+ ],
+ },
+ }),
yaml(),
prerender({ file: 'index.html', source: 'src/index.tsx' }),
prerender({ file: 'password-reset.html', source: 'src/password-reset/index.tsx' }),