perf(components): prevent rerenders from updatingState

Use Zustand selectors with custom equality functions to only subscribe to specific fields needed by components, avoiding unnecessary rerenders when transient state like updatingState changes. Extract offline indicators into separate components to isolate rerenders. Memoize card components in catalog and popular threads views.
This commit is contained in:
plebeius
2026-01-02 16:42:16 +01:00
parent 764b4258e6
commit fa32833b2a
15 changed files with 446 additions and 322 deletions
+48 -61
View File
@@ -1,82 +1,69 @@
import { useEffect, useState } from 'react';
import { useMemo, useRef } from 'react';
import { Subplebbit } from '@plebbit/plebbit-react-hooks';
import { getCommentMediaInfo, getHasThumbnail } from '../lib/utils/media-utils';
/**
* Extracts popular posts from subplebbits.
* Uses memoization to avoid recomputing when only updatingState changes.
*/
const usePopularPosts = (subplebbits: Subplebbit[]) => {
const [popularPosts, setPopularPosts] = useState<Comment[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Track the previous CID list to detect actual content changes vs transient state changes
const prevCidsRef = useRef<string>('');
useEffect(() => {
const fetchPopularPosts = () => {
try {
setIsLoading(true);
setError(null);
const uniqueLinks: Set<string> = new Set();
const allPosts: Comment[] = [];
const { popularPosts, error } = useMemo(() => {
try {
const uniqueLinks: Set<string> = new Set();
const allPosts: Comment[] = [];
const postsPerSub = [0, 8, 4, 3, 2, 2, 2, 2, 1][Math.min(subplebbits.length, 8)];
const postsPerSub = [0, 8, 4, 3, 2, 2, 2, 2, 1][Math.min(subplebbits.length, 8)];
subplebbits.forEach((subplebbit: any) => {
let subplebbitPosts: Comment[] = [];
subplebbits.forEach((subplebbit: any) => {
let subplebbitPosts: Comment[] = [];
if (subplebbit?.posts?.pages?.hot?.comments) {
for (const post of Object.values(subplebbit.posts.pages.hot.comments as Comment)) {
const {
deleted,
link,
linkHeight,
linkWidth,
locked,
pinned,
removed,
replyCount,
thumbnailUrl,
// timestamp
} = post;
if (subplebbit?.posts?.pages?.hot?.comments) {
for (const post of Object.values(subplebbit.posts.pages.hot.comments as Comment)) {
const { deleted, link, linkHeight, linkWidth, locked, pinned, removed, replyCount, thumbnailUrl } = post;
try {
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
try {
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
if (
hasThumbnail &&
replyCount > 1 &&
!deleted &&
!removed &&
!locked &&
!pinned &&
// timestamp > Date.now() / 1000 - 60 * 60 * 24 * 30 &&
!uniqueLinks.has(link)
) {
subplebbitPosts.push(post);
uniqueLinks.add(link);
}
} catch (err) {
console.error('Error processing post:', err);
if (hasThumbnail && replyCount > 1 && !deleted && !removed && !locked && !pinned && !uniqueLinks.has(link)) {
subplebbitPosts.push(post);
uniqueLinks.add(link);
}
} catch (err) {
console.error('Error processing post:', err);
}
subplebbitPosts.sort((a: any, b: any) => b.timestamp - a.timestamp);
allPosts.push(...subplebbitPosts.slice(0, postsPerSub));
}
});
const sortedPosts = allPosts.sort((a: any, b: any) => b.timestamp - a.timestamp).slice(0, 8);
subplebbitPosts.sort((a: any, b: any) => b.timestamp - a.timestamp);
allPosts.push(...subplebbitPosts.slice(0, postsPerSub));
}
});
setPopularPosts(sortedPosts);
} catch (err) {
console.error('Error in usePopularPosts:', err);
setError('Failed to fetch popular posts');
} finally {
setIsLoading(false);
}
};
const sortedPosts = allPosts.sort((a: any, b: any) => b.timestamp - a.timestamp).slice(0, 8);
fetchPopularPosts();
return { popularPosts: sortedPosts, error: null };
} catch (err) {
console.error('Error in usePopularPosts:', err);
return { popularPosts: [], error: 'Failed to fetch popular posts' };
}
}, [subplebbits]);
return { popularPosts, isLoading, error };
// Create stable reference: only update if the CIDs actually change
// This prevents unnecessary rerenders when only updatingState changes
const currentCids = popularPosts.map((p: any) => p.cid).join(',');
const stablePostsRef = useRef<Comment[]>(popularPosts);
if (currentCids !== prevCidsRef.current) {
prevCidsRef.current = currentCids;
stablePostsRef.current = popularPosts;
}
const isLoading = stablePostsRef.current.length === 0;
return { popularPosts: stablePostsRef.current, isLoading, error };
};
export default usePopularPosts;