From b10ada55e7c7603a9d1680ee4240e120655e0798 Mon Sep 17 00:00:00 2001 From: plebeius Date: Thu, 16 Oct 2025 23:13:18 +0200 Subject: [PATCH] perf: eliminate redundant derived state in board and post views --- src/hooks/use-comment-media-info.ts | 74 +++++++++++++++++------------ src/views/board/board.tsx | 10 +--- src/views/catalog/catalog.tsx | 3 +- src/views/post/post.tsx | 9 +--- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/hooks/use-comment-media-info.ts b/src/hooks/use-comment-media-info.ts index bf646401..99c9a7fd 100644 --- a/src/hooks/use-comment-media-info.ts +++ b/src/hooks/use-comment-media-info.ts @@ -1,42 +1,56 @@ -import { useCallback, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useLocation, useParams } from 'react-router-dom'; -import { getCommentMediaInfo, fetchWebpageThumbnailIfNeeded } from '../lib/utils/media-utils'; +import { getCommentMediaInfo, fetchWebpageThumbnailIfNeeded, CommentMediaInfo } from '../lib/utils/media-utils'; import { isPendingPostView, isPostPageView } from '../lib/utils/view-utils'; -export const useCommentMediaInfo = (link: string, thumbnailUrl: string, linkWidth: number, linkHeight: number) => { +/** + * Hook to fetch and cache media info with thumbnail dimensions for comments. + * Properly tracks thumbnail dimensions using state so they're available after the image loads. + */ +export const useCommentMediaInfo = (link: string, thumbnailUrl: string, linkWidth: number, linkHeight: number): CommentMediaInfo | undefined => { const location = useLocation(); const params = useParams(); const isInPostPageView = isPostPageView(location.pathname, params); const isInPendingPostView = isPendingPostView(location.pathname, params); - // some sites have CORS access, so the thumbnail can be fetched client-side, which is helpful if subplebbit.settings.fetchThumbnailUrls is false - const fetchThumbnail = useCallback(async () => { - let commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight); - if (commentMediaInfo?.type === 'webpage' && !commentMediaInfo.thumbnail) { - const newMediaInfo = await fetchWebpageThumbnailIfNeeded(commentMediaInfo); - // Fetch the dimensions of the thumbnail - if (newMediaInfo.thumbnail) { - const img = new Image(); - img.onload = () => { - commentMediaInfo = { - ...newMediaInfo, - thumbnailWidth: img.width, - thumbnailHeight: img.height, - }; - }; - img.src = newMediaInfo.thumbnail; - } - commentMediaInfo = newMediaInfo; - } - return commentMediaInfo; - }, [link, thumbnailUrl, linkWidth, linkHeight]); + const [thumbnailDimensions, setThumbnailDimensions] = useState<{ width: number; height: number } | null>(null); + // Fetch and cache thumbnail dimensions for webpage media useEffect(() => { - // don't fetch in feed view, it displaces the posts - if (isInPostPageView && isInPendingPostView) { - fetchThumbnail(); - } - }, [fetchThumbnail, isInPostPageView, isInPendingPostView]); + if (!(isInPostPageView || isInPendingPostView)) return; - return getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight); + const fetchAndCacheThumbnail = async () => { + const mediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight); + + if (mediaInfo?.type === 'webpage' && !mediaInfo.thumbnail) { + const newMediaInfo = await fetchWebpageThumbnailIfNeeded(mediaInfo); + + if (newMediaInfo.thumbnail) { + const img = new Image(); + img.onload = () => { + setThumbnailDimensions({ width: img.width, height: img.height }); + }; + img.onerror = () => { + // Silently handle failed image loads + }; + img.src = newMediaInfo.thumbnail; + } + } + }; + + fetchAndCacheThumbnail(); + }, [link, thumbnailUrl, linkWidth, linkHeight, isInPostPageView, isInPendingPostView]); + + const mediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight); + + // Return media info with cached thumbnail dimensions if available + if (thumbnailDimensions && mediaInfo) { + return { + ...mediaInfo, + thumbnailWidth: thumbnailDimensions.width, + thumbnailHeight: thumbnailDimensions.height, + }; + } + + return mediaInfo; }; diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx index c76bf5ec..5be4f1e0 100644 --- a/src/views/board/board.tsx +++ b/src/views/board/board.tsx @@ -269,15 +269,7 @@ const Board = () => { document.title = boardTitle + ' - plebchan'; }, [title, shortAddress, subplebbitAddress]); - // probably not necessary to show the error to the user if the feed loaded successfully - const [shouldShowErrorToUser, setShouldShowErrorToUser] = useState(false); - useEffect(() => { - if (error?.message && feed.length === 0) { - setShouldShowErrorToUser(true); - } else if (feed.length > 0) { - setShouldShowErrorToUser(false); - } - }, [error, feed]); + const shouldShowErrorToUser = error?.message && feed.length === 0; return ( <> diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx index d3b710a3..a8da37fb 100644 --- a/src/views/catalog/catalog.tsx +++ b/src/views/catalog/catalog.tsx @@ -462,7 +462,7 @@ const Catalog = () => { }, [clearMatchedFilters, subplebbitAddress]); // Apply filter colors to posts when feed changes - useEffect(() => { + const coloredFeed = useMemo(() => { if (combinedFeed.length > 0 && filterItems.length > 0) { // Clear existing matched filters clearMatchedFilters(); @@ -482,6 +482,7 @@ const Catalog = () => { } }); } + return combinedFeed; }, [combinedFeed, filterItems, clearMatchedFilters]); return ( diff --git a/src/views/post/post.tsx b/src/views/post/post.tsx index 34d3ba7e..5fb7b3a1 100644 --- a/src/views/post/post.tsx +++ b/src/views/post/post.tsx @@ -85,14 +85,7 @@ const PostPage = () => { }, [title, shortAddress, subplebbitAddress, post?.title, post?.content, isInAllView, t]); // probably not necessary to show the error to the user if the post loaded successfully - const [shouldShowErrorToUser, setShouldShowErrorToUser] = useState(false); - useEffect(() => { - if (post?.error && ((post?.replyCount > 0 && post?.replies?.length === 0) || (post?.state === 'failed' && post?.error))) { - setShouldShowErrorToUser(true); - } else if (post?.replyCount > 0 && post?.replies?.length > 0) { - setShouldShowErrorToUser(false); - } - }, [post]); + const shouldShowErrorToUser = post?.error && ((post?.replyCount > 0 && post?.replies?.length === 0) || (post?.state === 'failed' && post?.error)); return (