mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
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:
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import { useSubplebbitField } from './use-stable-subplebbit';
|
||||
|
||||
interface AuthorPrivilegesProps {
|
||||
commentAuthorAddress: string;
|
||||
@@ -11,8 +11,8 @@ interface AuthorPrivilegesProps {
|
||||
const useAuthorPrivileges = ({ commentAuthorAddress, subplebbitAddress }: AuthorPrivilegesProps) => {
|
||||
const account = useAccount();
|
||||
const accountAuthorAddress = account?.author?.address;
|
||||
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]);
|
||||
const { roles } = subplebbit || {};
|
||||
// Only subscribe to roles field to avoid rerenders from updatingState changes
|
||||
const roles = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.roles);
|
||||
const { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor, commentAuthorRole, accountAuthorRole } = useMemo(() => {
|
||||
const commentAuthorRole = roles?.[commentAuthorAddress]?.role;
|
||||
const isCommentAuthorMod = commentAuthorRole === 'admin' || commentAuthorRole === 'owner' || commentAuthorRole === 'moderator';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useMemo } from 'react';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
|
||||
/**
|
||||
* Custom equality function that ignores transient state properties
|
||||
* like updatingState, state, errors, etc. Only compares stable content fields.
|
||||
*/
|
||||
const isSubplebbitEqual = (prev: any, next: any): boolean => {
|
||||
if (prev === next) return true;
|
||||
if (!prev || !next) return prev === next;
|
||||
|
||||
// Compare only stable fields, ignore transient state
|
||||
return (
|
||||
prev.address === next.address &&
|
||||
prev.title === next.title &&
|
||||
prev.shortAddress === next.shortAddress &&
|
||||
prev.roles === next.roles &&
|
||||
prev.updatedAt === next.updatedAt &&
|
||||
prev.createdAt === next.createdAt &&
|
||||
prev.description === next.description
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get a subplebbit with stable reference that ignores updatingState changes.
|
||||
* Use this when you only need content fields and don't care about loading states.
|
||||
*
|
||||
* @param subplebbitAddress - The address of the subplebbit to retrieve
|
||||
* @returns The subplebbit object, or undefined if not found
|
||||
*/
|
||||
export const useStableSubplebbit = (subplebbitAddress: string | undefined) => {
|
||||
// Use selector with custom equality to ignore transient state
|
||||
const subplebbit = useSubplebbitsStore((state) => (subplebbitAddress ? state.subplebbits[subplebbitAddress] : undefined), isSubplebbitEqual);
|
||||
|
||||
return subplebbit;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get only specific fields from a subplebbit, ignoring updatingState.
|
||||
* This is more efficient when you only need a few fields.
|
||||
*
|
||||
* @param subplebbitAddress - The address of the subplebbit
|
||||
* @param selector - Function to extract the needed fields
|
||||
* @returns The selected fields
|
||||
*/
|
||||
export const useSubplebbitField = <T>(subplebbitAddress: string | undefined, selector: (subplebbit: any) => T): T | undefined => {
|
||||
const field = useSubplebbitsStore(
|
||||
(state) => {
|
||||
const subplebbit = subplebbitAddress ? state.subplebbits[subplebbitAddress] : undefined;
|
||||
return subplebbit ? selector(subplebbit) : undefined;
|
||||
},
|
||||
(prev, next) => prev === next,
|
||||
);
|
||||
|
||||
return field;
|
||||
};
|
||||
Reference in New Issue
Block a user