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' }),