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.
This commit is contained in:
Uruk
2026-01-14 10:24:57 +01:00
parent 32f4bbcc7d
commit 12047cbe12
24 changed files with 68 additions and 53 deletions

View File

@@ -26,7 +26,7 @@ export const TrackSheet: React.FC<Props> = ({
const streams = useMemo(
() => source?.MediaStreams?.filter((x) => x.Type === streamType),
[source],
[source, streamType],
);
const selectedSteam = useMemo(

View File

@@ -51,7 +51,7 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
api,
item: library,
}),
[library],
[api, library],
);
const itemType = useMemo(() => {

View File

@@ -23,6 +23,7 @@ export const MusicAlbumCard: React.FC<Props> = ({ album, width = 130 }) => {
);
const handlePress = useCallback(() => {
if (!album.Id) return;
router.push(`/music/album/${album.Id}`);
}, [router, album.Id]);

View File

@@ -24,6 +24,7 @@ export const MusicAlbumRowCard: React.FC<Props> = ({ album }) => {
);
const handlePress = useCallback(() => {
if (!album.Id) return;
router.push(`/music/album/${album.Id}`);
}, [router, album.Id]);

View File

@@ -25,6 +25,7 @@ export const MusicArtistCard: React.FC<Props> = ({ artist }) => {
);
const handlePress = useCallback(() => {
if (!artist.Id) return;
router.push(`/music/artist/${artist.Id}`);
}, [router, artist.Id]);

View File

@@ -170,7 +170,7 @@ const SeerrPoster: React.FC<Props> = ({
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 && (
<Tags
className='absolute bottom-1 left-0.5 w-32'
tagProps={{

View File

@@ -20,12 +20,13 @@ const CastSlide: React.FC<
horizontal
showsHorizontalScrollIndicator={false}
data={details?.credits.cast}
estimatedItemSize={112}
ItemSeparatorComponent={() => <View className='w-2' />}
keyExtractor={(item) => item?.id?.toString()}
keyExtractor={(item) => item?.id?.toString() ?? ""}
contentContainerStyle={{ paddingHorizontal: 16 }}
renderItem={({ item }) => (
<PersonPoster
id={item.id.toString()}
id={item?.id?.toString() ?? ""}
posterPath={item.profilePath}
name={item.name}
subName={item.character}

View File

@@ -58,7 +58,7 @@ const DetailFacts: React.FC<
(details as MovieDetails)?.releases?.results.find(
(r: TmdbRelease) => 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]);

View File

@@ -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<Props> = ({ index }) => {
export const GridSkeleton = React.memo(() => {
return (
<View
key={index}
className='flex flex-col mr-2 h-auto'
style={{ width: "30.5%" }}
>
<View className='flex flex-col mr-2 h-auto' style={{ width: "30.5%" }}>
<View className='relative rounded-lg overflow-hidden border border-neutral-900 w-full mt-4 aspect-[10/15] bg-neutral-800' />
<View className='mt-2 flex flex-col w-full'>
<View className='h-4 bg-neutral-800 rounded mb-1' />
@@ -18,4 +12,4 @@ export const GridSkeleton: React.FC<Props> = ({ index }) => {
</View>
</View>
);
};
});

View File

@@ -40,6 +40,8 @@ const PersonPoster: React.FC<Props & ViewProps> = ({
</View>
</TouchableOpacity>
);
return null;
};
export default PersonPoster;

View File

@@ -38,7 +38,16 @@ const RequestModal = forwardRef<
Props & Omit<ViewProps, "id">
>(
(
{ 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 (

View File

@@ -113,7 +113,7 @@ export const SeerrIndexPage: React.FC<Props> = ({
],
order || "desc",
),
[seerrResults, sortingType, order],
[seerrResults, sortingType, order, searchQuery],
);
const seerrTvResults = useMemo(
@@ -125,7 +125,7 @@ export const SeerrIndexPage: React.FC<Props> = ({
],
order || "desc",
),
[seerrResults, sortingType, order],
[seerrResults, sortingType, order, searchQuery],
);
const seerrPersonResults = useMemo(
@@ -137,7 +137,7 @@ export const SeerrIndexPage: React.FC<Props> = ({
],
order || "desc",
),
[seerrResults, sortingType, order],
[seerrResults, sortingType, order, searchQuery],
);
if (!searchQuery.length)

View File

@@ -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 (

View File

@@ -23,7 +23,6 @@ const Discover: React.FC<Props> = ({ sliders }) => {
sortBy(
(sliders ?? []).filter((s) => s.enabled),
"order",
"asc",
),
[sliders],
);

View File

@@ -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);

View File

@@ -23,9 +23,8 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ 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 () => {

View File

@@ -64,7 +64,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
.flatMap((p) => p?.results.filter((r) => isSeerrMovieOrTvResult(r))),
"id",
),
[data],
[data, isSeerrMovieOrTvResult],
);
return (

View File

@@ -14,10 +14,7 @@ export interface SlideProps {
interface Props<T> extends SlideProps {
data: T[];
renderItem: (
item: T,
index: number,
) => React.ComponentType<any> | React.ReactElement | null | undefined;
renderItem: (item: T, index: number) => React.ReactElement | null;
keyExtractor: (item: T) => string;
onEndReached?: (() => void) | null | undefined;
}