From 12047cbe1267c5141e2dda5ad92ec592ff7d1d92 Mon Sep 17 00:00:00 2001 From: Uruk Date: Wed, 14 Jan 2026 10:24:57 +0100 Subject: [PATCH] fix: correct dependency arrays and add null checks Fixes missing dependencies in useMemo and useCallback hooks to prevent stale closures and potential bugs. Adds null/undefined guards before navigation in music components to prevent crashes when attempting to navigate with missing IDs. Corrects query key from "company" to "genre" in genre page to ensure proper cache invalidation. Updates Jellyseerr references to Seerr throughout documentation and error messages for consistency. Improves type safety by adding error rejection handling in SeerrApi and memoizing components to optimize re-renders. --- CLAUDE.md | 2 +- .../seerr/company/[companyId].tsx | 2 +- .../seerr/genre/[genreId].tsx | 2 +- components/TrackSheet.tsx | 2 +- components/library/LibraryItemCard.tsx | 2 +- components/music/MusicAlbumCard.tsx | 1 + components/music/MusicAlbumRowCard.tsx | 1 + components/music/MusicArtistCard.tsx | 1 + components/posters/SeerrPoster.tsx | 2 +- components/seerr/Cast.tsx | 5 ++-- components/seerr/DetailFacts.tsx | 12 ++++----- components/seerr/GridSkeleton.tsx | 14 +++-------- components/seerr/PersonPoster.tsx | 2 ++ components/seerr/RequestModal.tsx | 21 ++++++++++++++-- components/seerr/SeerrIndexPage.tsx | 6 ++--- components/seerr/discover/CompanySlide.tsx | 2 +- components/seerr/discover/Discover.tsx | 1 - .../seerr/discover/GenericSlideCard.tsx | 4 +-- components/seerr/discover/GenreSlide.tsx | 3 +-- components/seerr/discover/MovieTvSlide.tsx | 2 +- components/seerr/discover/Slide.tsx | 5 +--- hooks/useSeerr.ts | 25 +++++++++++-------- scripts/typecheck.js | 2 +- translations/en.json | 2 +- 24 files changed, 68 insertions(+), 53 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cc3b0a53..5cfd2990 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Streamyfin is a cross-platform Jellyfin video streaming client built with Expo (React Native). It supports mobile (iOS/Android) and TV platforms, with features including offline downloads, Chromecast support, and Jellyseerr integration. +Streamyfin is a cross-platform Jellyfin video streaming client built with Expo (React Native). It supports mobile (iOS/Android) and TV platforms, with features including offline downloads, Chromecast support, and Seerr integration. ## Development Commands diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/company/[companyId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/company/[companyId].tsx index 359ebfd1..4f129694 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/company/[companyId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/company/[companyId].tsx @@ -57,7 +57,7 @@ export default function CompanyPage() { ), "id", ) ?? [], - [data], + [data, isSeerrMovieOrTvResult], ); const backdrops = useMemo( diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/genre/[genreId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/genre/[genreId].tsx index a61cff40..5858d96e 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/genre/[genreId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/genre/[genreId].tsx @@ -20,7 +20,7 @@ export default function GenrePage() { }; const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ - queryKey: ["seerr", "company", type, genreId], + queryKey: ["seerr", "genre", type, genreId], queryFn: async ({ pageParam }) => { const params: any = { page: Number(pageParam), diff --git a/components/TrackSheet.tsx b/components/TrackSheet.tsx index 4dda0564..8c5f3837 100644 --- a/components/TrackSheet.tsx +++ b/components/TrackSheet.tsx @@ -26,7 +26,7 @@ export const TrackSheet: React.FC = ({ const streams = useMemo( () => source?.MediaStreams?.filter((x) => x.Type === streamType), - [source], + [source, streamType], ); const selectedSteam = useMemo( diff --git a/components/library/LibraryItemCard.tsx b/components/library/LibraryItemCard.tsx index 09de500d..c84364ce 100644 --- a/components/library/LibraryItemCard.tsx +++ b/components/library/LibraryItemCard.tsx @@ -51,7 +51,7 @@ export const LibraryItemCard: React.FC = ({ library, ...props }) => { api, item: library, }), - [library], + [api, library], ); const itemType = useMemo(() => { diff --git a/components/music/MusicAlbumCard.tsx b/components/music/MusicAlbumCard.tsx index 11e78610..773b96c0 100644 --- a/components/music/MusicAlbumCard.tsx +++ b/components/music/MusicAlbumCard.tsx @@ -23,6 +23,7 @@ export const MusicAlbumCard: React.FC = ({ album, width = 130 }) => { ); const handlePress = useCallback(() => { + if (!album.Id) return; router.push(`/music/album/${album.Id}`); }, [router, album.Id]); diff --git a/components/music/MusicAlbumRowCard.tsx b/components/music/MusicAlbumRowCard.tsx index e794a793..ffb4b6cb 100644 --- a/components/music/MusicAlbumRowCard.tsx +++ b/components/music/MusicAlbumRowCard.tsx @@ -24,6 +24,7 @@ export const MusicAlbumRowCard: React.FC = ({ album }) => { ); const handlePress = useCallback(() => { + if (!album.Id) return; router.push(`/music/album/${album.Id}`); }, [router, album.Id]); diff --git a/components/music/MusicArtistCard.tsx b/components/music/MusicArtistCard.tsx index a9bfc61b..f3ccb95b 100644 --- a/components/music/MusicArtistCard.tsx +++ b/components/music/MusicArtistCard.tsx @@ -25,6 +25,7 @@ export const MusicArtistCard: React.FC = ({ artist }) => { ); const handlePress = useCallback(() => { + if (!artist.Id) return; router.push(`/music/artist/${artist.Id}`); }, [router, artist.Id]); diff --git a/components/posters/SeerrPoster.tsx b/components/posters/SeerrPoster.tsx index 399ac386..d9c0c425 100644 --- a/components/posters/SeerrPoster.tsx +++ b/components/posters/SeerrPoster.tsx @@ -170,7 +170,7 @@ const SeerrPoster: React.FC = ({ className='absolute right-1 top-1 text-right bg-black border border-neutral-800/50' text={mediaRequest?.requestedBy.displayName} /> - {requestedSeasons.length > 0 && ( + {(requestedSeasons?.length ?? 0) > 0 && ( } - keyExtractor={(item) => item?.id?.toString()} + keyExtractor={(item) => item?.id?.toString() ?? ""} contentContainerStyle={{ paddingHorizontal: 16 }} renderItem={({ item }) => ( r.iso_3166_1 === region, )?.release_dates as TmdbRelease["release_dates"], - [details], + [details, region], ); // Release date types: @@ -82,7 +82,7 @@ const DetailFacts: React.FC< if (firstAirDate) { return new Date(firstAirDate).toLocaleDateString(locale, dateOpts); } - }, [details]); + }, [details, locale]); const nextAirDate = useMemo(() => { const firstAirDate = (details as TvDetails)?.firstAirDate; @@ -90,7 +90,7 @@ const DetailFacts: React.FC< if (nextAirDate && firstAirDate !== nextAirDate) { return new Date(nextAirDate).toLocaleDateString(locale, dateOpts); } - }, [details]); + }, [details, locale]); const revenue = useMemo( () => @@ -98,7 +98,7 @@ const DetailFacts: React.FC< style: "currency", currency: "USD", }), - [details], + [details, locale], ); const budget = useMemo( @@ -107,7 +107,7 @@ const DetailFacts: React.FC< style: "currency", currency: "USD", }), - [details], + [details, locale], ); const streamingProviders = useMemo( @@ -115,7 +115,7 @@ const DetailFacts: React.FC< details?.watchProviders?.find( (provider) => provider.iso_3166_1 === region, )?.flatrate, - [details], + [details, region], ); const networks = useMemo(() => (details as TvDetails)?.networks, [details]); diff --git a/components/seerr/GridSkeleton.tsx b/components/seerr/GridSkeleton.tsx index 75431cd7..a7890312 100644 --- a/components/seerr/GridSkeleton.tsx +++ b/components/seerr/GridSkeleton.tsx @@ -1,16 +1,10 @@ +import React from "react"; import { View } from "react-native"; -interface Props { - index: number; -} // Dev note might be a good idea to standardize skeletons across the app and have one "file" for it. -export const GridSkeleton: React.FC = ({ index }) => { +export const GridSkeleton = React.memo(() => { return ( - + @@ -18,4 +12,4 @@ export const GridSkeleton: React.FC = ({ index }) => { ); -}; +}); diff --git a/components/seerr/PersonPoster.tsx b/components/seerr/PersonPoster.tsx index 821cf52c..e9b77b55 100644 --- a/components/seerr/PersonPoster.tsx +++ b/components/seerr/PersonPoster.tsx @@ -40,6 +40,8 @@ const PersonPoster: React.FC = ({ ); + + return null; }; export default PersonPoster; diff --git a/components/seerr/RequestModal.tsx b/components/seerr/RequestModal.tsx index 53d23765..b3069458 100644 --- a/components/seerr/RequestModal.tsx +++ b/components/seerr/RequestModal.tsx @@ -38,7 +38,16 @@ const RequestModal = forwardRef< Props & Omit >( ( - { id, title, requestBody, type, isAnime = false, onRequested, onDismiss }, + { + id, + title, + requestBody, + type, + isAnime = false, + is4k, + onRequested, + onDismiss, + }, ref, ) => { const { seerrApi, seerrUser, requestMedia } = useSeerr(); @@ -258,7 +267,8 @@ const RequestModal = forwardRef< const request = useCallback(() => { const body = { - is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k, + is4k: + is4k ?? defaultService?.is4k ?? defaultServiceDetails?.server.is4k, profileId: defaultProfile?.id, rootFolder: defaultFolder?.path, tags: defaultTags.map((t) => t.id), @@ -274,11 +284,18 @@ const RequestModal = forwardRef< onRequested, ); }, [ + is4k, + defaultService?.is4k, + defaultServiceDetails?.server.is4k, requestBody, requestOverrides, defaultProfile, defaultFolder, defaultTags, + requestMedia, + seasonTitle, + title, + onRequested, ]); return ( diff --git a/components/seerr/SeerrIndexPage.tsx b/components/seerr/SeerrIndexPage.tsx index 6ef54fa6..edfd2b0d 100644 --- a/components/seerr/SeerrIndexPage.tsx +++ b/components/seerr/SeerrIndexPage.tsx @@ -113,7 +113,7 @@ export const SeerrIndexPage: React.FC = ({ ], order || "desc", ), - [seerrResults, sortingType, order], + [seerrResults, sortingType, order, searchQuery], ); const seerrTvResults = useMemo( @@ -125,7 +125,7 @@ export const SeerrIndexPage: React.FC = ({ ], order || "desc", ), - [seerrResults, sortingType, order], + [seerrResults, sortingType, order, searchQuery], ); const seerrPersonResults = useMemo( @@ -137,7 +137,7 @@ export const SeerrIndexPage: React.FC = ({ ], order || "desc", ), - [seerrResults, sortingType, order], + [seerrResults, sortingType, order, searchQuery], ); if (!searchQuery.length) diff --git a/components/seerr/discover/CompanySlide.tsx b/components/seerr/discover/CompanySlide.tsx index 58e614fa..f29d849e 100644 --- a/components/seerr/discover/CompanySlide.tsx +++ b/components/seerr/discover/CompanySlide.tsx @@ -26,7 +26,7 @@ const CompanySlide: React.FC< pathname: `/(auth)/(tabs)/${from}/seerr/company/${id}` as any, params: { id, image, name, type: slide.type }, }), - [slide], + [router, from, slide.type], ); return ( diff --git a/components/seerr/discover/Discover.tsx b/components/seerr/discover/Discover.tsx index 7a926eec..bd099359 100644 --- a/components/seerr/discover/Discover.tsx +++ b/components/seerr/discover/Discover.tsx @@ -23,7 +23,6 @@ const Discover: React.FC = ({ sliders }) => { sortBy( (sliders ?? []).filter((s) => s.enabled), "order", - "asc", ), [sliders], ); diff --git a/components/seerr/discover/GenericSlideCard.tsx b/components/seerr/discover/GenericSlideCard.tsx index 5ee68dd9..36b9ab4d 100644 --- a/components/seerr/discover/GenericSlideCard.tsx +++ b/components/seerr/discover/GenericSlideCard.tsx @@ -1,6 +1,6 @@ import { Image, type ImageContentFit } from "expo-image"; import { LinearGradient } from "expo-linear-gradient"; -import type React from "react"; +import React from "react"; import { StyleSheet, View, type ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; @@ -67,4 +67,4 @@ const GenericSlideCard: React.FC< ); -export default GenericSlideCard; +export default React.memo(GenericSlideCard); diff --git a/components/seerr/discover/GenreSlide.tsx b/components/seerr/discover/GenreSlide.tsx index e65534d0..7f1a6a83 100644 --- a/components/seerr/discover/GenreSlide.tsx +++ b/components/seerr/discover/GenreSlide.tsx @@ -23,9 +23,8 @@ const GenreSlide: React.FC = ({ slide, ...props }) => { pathname: `/(auth)/(tabs)/${from}/seerr/genre/${genre.id}` as any, params: { type: slide.type, name: genre.name }, }), - [slide], + [router, from, slide.type], ); - const { data } = useQuery({ queryKey: ["seerr", "discover", slide.type, slide.id], queryFn: async () => { diff --git a/components/seerr/discover/MovieTvSlide.tsx b/components/seerr/discover/MovieTvSlide.tsx index 6291bab1..d698701c 100644 --- a/components/seerr/discover/MovieTvSlide.tsx +++ b/components/seerr/discover/MovieTvSlide.tsx @@ -64,7 +64,7 @@ const MovieTvSlide: React.FC = ({ .flatMap((p) => p?.results.filter((r) => isSeerrMovieOrTvResult(r))), "id", ), - [data], + [data, isSeerrMovieOrTvResult], ); return ( diff --git a/components/seerr/discover/Slide.tsx b/components/seerr/discover/Slide.tsx index 41b4667e..7f794a3c 100644 --- a/components/seerr/discover/Slide.tsx +++ b/components/seerr/discover/Slide.tsx @@ -14,10 +14,7 @@ export interface SlideProps { interface Props extends SlideProps { data: T[]; - renderItem: ( - item: T, - index: number, - ) => React.ComponentType | React.ReactElement | null | undefined; + renderItem: (item: T, index: number) => React.ReactElement | null; keyExtractor: (item: T) => string; onEndReached?: (() => void) | null | undefined; } diff --git a/hooks/useSeerr.ts b/hooks/useSeerr.ts index 679677d4..c0e7ce3a 100644 --- a/hooks/useSeerr.ts +++ b/hooks/useSeerr.ts @@ -424,6 +424,7 @@ export class SeerrApi { }, (error) => { console.error("Seerr request error", error); + return Promise.reject(error); }, ); } @@ -448,7 +449,7 @@ export const useSeerr = () => { clearSeerrStorageData(); setSeerrUser(undefined); updateSettings({ seerrServerUrl: undefined }); - }, []); + }, [setSeerrUser, updateSettings]); const requestMedia = useCallback( (title: string, request: MediaRequestBody, onSuccess?: () => void) => { @@ -474,18 +475,20 @@ export const useSeerr = () => { } }); }, - [seerrApi], + [seerrApi, queryClient], ); - const isSeerrMovieOrTvResult = ( - items: any | null | undefined, - ): items is MovieResult | TvResult => { - return ( - items && - Object.hasOwn(items, "mediaType") && - (items.mediaType === MediaType.MOVIE || items.mediaType === MediaType.TV) - ); - }; + const isSeerrMovieOrTvResult = useCallback( + (items: any | null | undefined): items is MovieResult | TvResult => { + return ( + items && + Object.hasOwn(items, "mediaType") && + (items.mediaType === MediaType.MOVIE || + items.mediaType === MediaType.TV) + ); + }, + [], + ); const getTitle = ( item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast, diff --git a/scripts/typecheck.js b/scripts/typecheck.js index 73914e8c..1771dc60 100644 --- a/scripts/typecheck.js +++ b/scripts/typecheck.js @@ -176,7 +176,7 @@ function runTypeCheck() { } catch (error) { const errorOutput = (error && (error.stderr || error.stdout)) || ""; - // Filter out jellyseerr utils errors - this is a third-party git submodule + // Filter out seerr utils errors - this is a third-party git submodule // that generates a large volume of known type errors const filteredLines = errorOutput.split("\n").filter((line) => { const trimmedLine = line.trim(); diff --git a/translations/en.json b/translations/en.json index 55a6c198..d6ae888a 100644 --- a/translations/en.json +++ b/translations/en.json @@ -719,7 +719,7 @@ "requested_by": "Requested by {{user}}", "unknown_user": "Unknown User", "toasts": { - "seer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0", + "seerr_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0", "seerr_test_failed": "Seerr test failed. Please try again.", "failed_to_test_seerr_server_url": "Failed to test Seerr server URL", "issue_submitted": "Issue Submitted!",