From 68fa451d19cd1ca3674fb20077cd9e6291718f83 Mon Sep 17 00:00:00 2001 From: Sean Edmunds Date: Mon, 12 Oct 2026 15:47:49 +0000 Subject: [PATCH 1/2] perf: optimize ThemePlaylists component by memoizing handlers, derived state, and stack objects --- .../Theming/ThemePlaylists/ThemePlaylists.tsx | 263 ++++++++++-------- 1 file changed, 141 insertions(+), 122 deletions(-) diff --git a/src/pages/settings/components/Theming/ThemePlaylists/ThemePlaylists.tsx b/src/pages/settings/components/Theming/ThemePlaylists/ThemePlaylists.tsx index 223d3c4..8ccc0b4 100644 --- a/src/pages/settings/components/Theming/ThemePlaylists/ThemePlaylists.tsx +++ b/src/pages/settings/components/Theming/ThemePlaylists/ThemePlaylists.tsx @@ -2,7 +2,7 @@ import { Box, Group, LoadingOverlay, SimpleGrid, Stack, Text } from '@mantine/co import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { NoItemsFound, SettingsHeader } from 'components'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useMemo, useCallback } from 'react'; import { FiChevronRight } from 'react-icons/fi'; import { textClasses } from 'styles'; import type { Theme, ThemePlaylist } from 'types'; @@ -21,13 +21,15 @@ export function ThemePlaylists() { }); const [searchValue, setSearchValue] = useState(''); const [debouncedSearch] = useDebouncedValue(searchValue, 500); - const themePlaylists: ThemePlaylist[] = - themePlaylistData?.filter( + + const themePlaylists: ThemePlaylist[] = useMemo(() => { + return themePlaylistData?.filter( (a: ThemePlaylist) => - a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || - a.name.includes('Time of Day'), + a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || + a.name.includes('Time of Day'), ) || []; - const defaultPlaylist = { + }, [themePlaylistData, debouncedSearch]); + const defaultPlaylist: ThemePlaylist = { id: undefined, default: 0, name: 'New Playlist', @@ -40,30 +42,73 @@ export function ThemePlaylists() { const { currentThemePlaylist, theme, isMobile } = useCommonHooks(); const [isLoading, setIsLoading] = useState(false); - const handleOpen = (playlist: ThemePlaylist) => { + const handleOpen = useCallback((playlist: ThemePlaylist) => { setSelectedPlaylist(playlist); open(); - }; + }, [open]); - const handleClose = () => { + const handleClose = useCallback(() => { setSelectedPlaylist(defaultPlaylist); close(); - }; + }, [close]); - const handleSavePlaylist = () => { + const handleSavePlaylist = useCallback(() => { mutateAsync({ method: selectedPlaylist?.id === undefined ? 'POST' : 'PATCH', endpoint: themePlaylistsEndpoint, body: selectedPlaylist ?? {}, }); close(); - }; + }, [selectedPlaylist, mutateAsync, themePlaylistsEndpoint, close]); useEffect(() => { if (debouncedSearch === searchValue) { setIsLoading(false); } - }, [debouncedSearch]); + }, [debouncedSearch, searchValue]); + + const allThemesIds = useMemo(() => + allThemeData?.map((a: Theme) => a.id) ?? [], + [allThemeData]); + + const backgroundsThemes = useMemo(() => + allThemeData?.filter((a: any) => !a.backgroundImage.toLowerCase().includes('none')) ?? [], + [allThemeData]); + + const noBackgroundsThemes = useMemo(() => + allThemeData?.filter((a: any) => a.backgroundImage.toLowerCase().includes('none')) ?? [], + [allThemeData]); + + const timeOfDayPlaylist = useMemo(() => { + const found = themePlaylists?.find((a: ThemePlaylist) => a.name === 'Time of Day'); + return { + id: found?.id, + name: 'Time of Day', + default: 1, + themeList: found?.themeList ?? '[]', + }; + }, [themePlaylists]); + + const allStack = useMemo(() => ({ + id: undefined, + name: 'All', + default: 1, + themeList: `[${allThemesIds.join(',')}]`, + }), [allThemesIds]); + + const backgroundsStack = useMemo(() => ({ + id: undefined, + name: 'Backgrounds', + default: 1, + themeList: `[${backgroundsThemes.map((a: Theme) => a.id).join(',')}]`, + }), [backgroundsThemes]); + + const noBackgroundsStack = useMemo(() => ({ + id: undefined, + name: 'No Backgrounds', + default: 1, + themeList: `[${noBackgroundsThemes.map((a: Theme) => a.id).join(',')}]`, + }), [noBackgroundsThemes]); return ( @@ -135,117 +180,91 @@ export function ThemePlaylists() { )} a.id) ?? ''}]`, - }} - handleOpen={handleOpen} - allThemes={allThemeData} - /> - - - - {'Backgrounds' === currentThemePlaylist && ( - - )} - - !a.backgroundImage.toLowerCase().includes('none')).map((a: Theme) => a.id) ?? ''}]`, - }} - handleOpen={handleOpen} - allThemes={allThemeData} - /> - - - - {'No Backgrounds' === currentThemePlaylist && ( - - )} - - a.backgroundImage.toLowerCase().includes('none')).map((a: Theme) => a.id) ?? ''}]`, - }} - handleOpen={handleOpen} - allThemes={allThemeData} - /> - - - - {'Time of Day' === currentThemePlaylist && ( - - )} - - a.name === 'Time of Day', - )?.id ?? undefined, - name: 'Time of Day', - default: 1, - themeList: - themePlaylists?.find( - (a: ThemePlaylist) => a.name === 'Time of Day', - )?.themeList ?? '[]', - }} + cardPlaylist={allStack} handleOpen={handleOpen} allThemes={allThemeData} /> + + {'Backgrounds' === currentThemePlaylist && ( + + )} + + + + + + {'No Backgrounds' === currentThemePlaylist && ( + + )} + + + + + + {'Time of Day' === currentThemePlaylist && ( + + )} + + + + )} @@ -293,7 +312,7 @@ export function ThemePlaylists() { cardPlaylist={cardPlaylist} handleOpen={handleOpen} allThemes={allThemeData} - /> + /> ); -- 2.43.0 From bf761df56d0fc5042b87621385fbfef0e3647eaa Mon Sep 17 00:00:00 2001 From: Sean Edmunds Date: Mon, 12 Oct 2026 11:17:21 -0500 Subject: [PATCH 2/2] Style fixes --- .../Theming/ThemePlaylists/ThemePlaylists.tsx | 425 +++++++++--------- 1 file changed, 225 insertions(+), 200 deletions(-) diff --git a/src/pages/settings/components/Theming/ThemePlaylists/ThemePlaylists.tsx b/src/pages/settings/components/Theming/ThemePlaylists/ThemePlaylists.tsx index 8ccc0b4..43bdaf5 100644 --- a/src/pages/settings/components/Theming/ThemePlaylists/ThemePlaylists.tsx +++ b/src/pages/settings/components/Theming/ThemePlaylists/ThemePlaylists.tsx @@ -2,7 +2,7 @@ import { Box, Group, LoadingOverlay, SimpleGrid, Stack, Text } from '@mantine/co import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { NoItemsFound, SettingsHeader } from 'components'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; -import { useEffect, useState, useMemo, useCallback } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { FiChevronRight } from 'react-icons/fi'; import { textClasses } from 'styles'; import type { Theme, ThemePlaylist } from 'types'; @@ -23,11 +23,13 @@ export function ThemePlaylists() { const [debouncedSearch] = useDebouncedValue(searchValue, 500); const themePlaylists: ThemePlaylist[] = useMemo(() => { - return themePlaylistData?.filter( - (a: ThemePlaylist) => - a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || - a.name.includes('Time of Day'), - ) || []; + return ( + themePlaylistData?.filter( + (a: ThemePlaylist) => + a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) || + a.name.includes('Time of Day'), + ) || [] + ); }, [themePlaylistData, debouncedSearch]); const defaultPlaylist: ThemePlaylist = { id: undefined, @@ -42,10 +44,13 @@ export function ThemePlaylists() { const { currentThemePlaylist, theme, isMobile } = useCommonHooks(); const [isLoading, setIsLoading] = useState(false); - const handleOpen = useCallback((playlist: ThemePlaylist) => { - setSelectedPlaylist(playlist); - open(); - }, [open]); + const handleOpen = useCallback( + (playlist: ThemePlaylist) => { + setSelectedPlaylist(playlist); + open(); + }, + [open], + ); const handleClose = useCallback(() => { setSelectedPlaylist(defaultPlaylist); @@ -67,17 +72,26 @@ export function ThemePlaylists() { } }, [debouncedSearch, searchValue]); - const allThemesIds = useMemo(() => - allThemeData?.map((a: Theme) => a.id) ?? [], - [allThemeData]); + const allThemesIds = useMemo( + () => allThemeData?.map((a: Theme) => a.id) ?? [], + [allThemeData], + ); - const backgroundsThemes = useMemo(() => - allThemeData?.filter((a: any) => !a.backgroundImage.toLowerCase().includes('none')) ?? [], - [allThemeData]); + const backgroundsThemes = useMemo( + () => + allThemeData?.filter( + (a: any) => !a.backgroundImage.toLowerCase().includes('none'), + ) ?? [], + [allThemeData], + ); - const noBackgroundsThemes = useMemo(() => - allThemeData?.filter((a: any) => a.backgroundImage.toLowerCase().includes('none')) ?? [], - [allThemeData]); + const noBackgroundsThemes = useMemo( + () => + allThemeData?.filter((a: any) => + a.backgroundImage.toLowerCase().includes('none'), + ) ?? [], + [allThemeData], + ); const timeOfDayPlaylist = useMemo(() => { const found = themePlaylists?.find((a: ThemePlaylist) => a.name === 'Time of Day'); @@ -89,26 +103,35 @@ export function ThemePlaylists() { }; }, [themePlaylists]); - const allStack = useMemo(() => ({ - id: undefined, - name: 'All', - default: 1, - themeList: `[${allThemesIds.join(',')}]`, - }), [allThemesIds]); + const allStack = useMemo( + () => ({ + id: undefined, + name: 'All', + default: 1, + themeList: `[${allThemesIds.join(',')}]`, + }), + [allThemesIds], + ); - const backgroundsStack = useMemo(() => ({ - id: undefined, - name: 'Backgrounds', - default: 1, - themeList: `[${backgroundsThemes.map((a: Theme) => a.id).join(',')}]`, - }), [backgroundsThemes]); + const backgroundsStack = useMemo( + () => ({ + id: undefined, + name: 'Backgrounds', + default: 1, + themeList: `[${backgroundsThemes.map((a: Theme) => a.id).join(',')}]`, + }), + [backgroundsThemes], + ); - const noBackgroundsStack = useMemo(() => ({ - id: undefined, - name: 'No Backgrounds', - default: 1, - themeList: `[${noBackgroundsThemes.map((a: Theme) => a.id).join(',')}]`, - }), [noBackgroundsThemes]); + const noBackgroundsStack = useMemo( + () => ({ + id: undefined, + name: 'No Backgrounds', + default: 1, + themeList: `[${noBackgroundsThemes.map((a: Theme) => a.id).join(',')}]`, + }), + [noBackgroundsThemes], + ); return ( @@ -153,7 +176,7 @@ export function ThemePlaylists() { /> {allThemeData && ( <> - + Defaults - - - - {'All' === currentThemePlaylist && ( - - )} - - - - - - - {'Backgrounds' === currentThemePlaylist && ( - - )} - - - - - - {'No Backgrounds' === currentThemePlaylist && ( - - )} - - - - - - {'Time of Day' === currentThemePlaylist && ( - - )} - - - - - - - User Made - - - - {themePlaylists.length > 1 ? ( - - {themePlaylists - .filter((playlist: ThemePlaylist) => playlist.default === 0) - .sort((a: ThemePlaylist, b: ThemePlaylist) => - a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? 1 : -1, - ) - .map((cardPlaylist: ThemePlaylist) => { - return ( - - {cardPlaylist.name === currentThemePlaylist && ( - - )} - - - - - ); - })} - - ) : ( - - + + + {'All' === currentThemePlaylist && ( + + )} + + + - )} - + + {'Backgrounds' === currentThemePlaylist && ( + + )} + + + + + + {'No Backgrounds' === currentThemePlaylist && ( + + )} + + + + + + {'Time of Day' === currentThemePlaylist && ( + + )} + + + + + + + + + + User Made + + + + {themePlaylists.length > 1 ? ( + + {themePlaylists + .filter((playlist: ThemePlaylist) => playlist.default === 0) + .sort((a: ThemePlaylist, b: ThemePlaylist) => + a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? 1 : -1, + ) + .map((cardPlaylist: ThemePlaylist) => { + return ( + + {cardPlaylist.name === currentThemePlaylist && ( + + )} + + + + + ); + })} + + ) : ( + + + + )} + + )} -- 2.43.0