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
+9 -4
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
import { Comment, useAccount, useAccountComments, useAccountSubplebbits, useBlock, useFeed, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { Comment, useAccount, useAccountComments, useAccountSubplebbits, useBlock, useFeed } from '@plebbit/plebbit-react-hooks';
import { useStableSubplebbit, useSubplebbitField } from '../../hooks/use-stable-subplebbit';
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
import { Trans, useTranslation } from 'react-i18next';
import styles from './board.module.css';
@@ -153,9 +154,13 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
}
}, [filteredComments, reset]);
const subplebbit = useSubplebbit({ subplebbitAddress });
const { error, shortAddress, state } = subplebbit || {};
const title = isInAllView ? t('all') : isInSubscriptionsView ? t('subscriptions') : isInModView ? t('mod') : subplebbit?.title;
// Use stable subplebbit fields to avoid rerenders from updatingState
const subplebbitTitle = useSubplebbitField(subplebbitAddress, (sub) => sub?.title);
const shortAddress = useSubplebbitField(subplebbitAddress, (sub) => sub?.shortAddress);
// Only subscribe to state and error for footer display - these are needed
const stableSubplebbit = useStableSubplebbit(subplebbitAddress);
const { error, state } = stableSubplebbit || {};
const title = isInAllView ? t('all') : isInSubscriptionsView ? t('subscriptions') : isInModView ? t('mod') : subplebbitTitle;
const { blocked, unblock } = useBlock({ address: subplebbitAddress });
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { memo, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Comment, Subplebbit } from '@plebbit/plebbit-react-hooks';
@@ -25,36 +25,41 @@ export const ContentPreview = ({ content, maxLength = 99 }: { content: string; m
return truncatedText;
};
const PopularThreadCard = ({ post, multisub }: PopularThreadProps) => {
const { cid, content, link, linkHeight, linkWidth, subplebbitAddress, thumbnailUrl, title } = post || {};
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const defaultSubplebbits = useDefaultSubplebbits();
// Memoize to prevent rerenders when parent rerenders due to updatingState
const PopularThreadCard = memo(
({ post, multisub }: PopularThreadProps) => {
const { cid, content, link, linkHeight, linkWidth, subplebbitAddress, thumbnailUrl, title } = post || {};
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const defaultSubplebbits = useDefaultSubplebbits();
// Find the matching MultisubSubplebbit entry and get its title
const multisubEntry = multisub.find((ms) => ms?.address === subplebbitAddress);
const boardTitle = multisubEntry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : '';
// Find the matching MultisubSubplebbit entry and get its title
const multisubEntry = multisub.find((ms) => ms?.address === subplebbitAddress);
const boardTitle = multisubEntry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : '';
return (
<div className={styles.popularThread} key={cid}>
<div className={styles.title}>{boardTitle}</div>
<div className={styles.mediaContainer}>
<Link to={`/${boardPath}/thread/${cid}`}>
<CatalogPostMedia commentMediaInfo={commentMediaInfo} isOutOfFeed={true} cid={cid} />
</Link>
return (
<div className={styles.popularThread} key={cid}>
<div className={styles.title}>{boardTitle}</div>
<div className={styles.mediaContainer}>
<Link to={`/${boardPath}/thread/${cid}`}>
<CatalogPostMedia commentMediaInfo={commentMediaInfo} isOutOfFeed={true} cid={cid} />
</Link>
</div>
<div className={styles.threadContent}>
{title && (
<>
<b>{title.trim()}</b>
{content && ': '}
</>
)}
{content && <ContentPreview content={content} maxLength={99} />}
</div>
</div>
<div className={styles.threadContent}>
{title && (
<>
<b>{title.trim()}</b>
{content && ': '}
</>
)}
{content && <ContentPreview content={content} maxLength={99} />}
</div>
</div>
);
};
);
},
// Custom equality: only rerender if post.cid changes
(prevProps, nextProps) => prevProps.post?.cid === nextProps.post?.cid,
);
const PopularThreadsBox = ({ multisub, subplebbits }: { multisub: MultisubSubplebbit[]; subplebbits: any }) => {
const { t } = useTranslation();
+4 -3
View File
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
import { getSubplebbitAddress } from '../../lib/utils/route-utils';
import { HomeLogo } from '../home';
@@ -20,8 +20,9 @@ const NotFound = () => {
const boardIdentifier = pathParts[0] && pathParts[0] !== 'not-found' && pathParts[0] !== 'faq' ? pathParts[0] : '';
const defaultSubplebbits = useDefaultSubplebbits();
const subplebbitAddress = boardIdentifier ? getSubplebbitAddress(boardIdentifier, defaultSubplebbits) : '';
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]);
const { address, shortAddress } = subplebbit || {};
// Only subscribe to address and shortAddress to avoid rerenders from updatingState changes
const address = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.address);
const shortAddress = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.shortAddress);
return (
<div className={styles.wrapper}>
+5 -3
View File
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Comment, Role, useComment, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { isAllView } from '../../lib/utils/view-utils';
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
@@ -27,7 +28,8 @@ export interface PostProps {
}
export const Post = ({ post, showAllReplies = false, showReplies = true }: PostProps) => {
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[post?.subplebbitAddress]);
// Only subscribe to roles field to avoid rerenders from updatingState changes
const roles = useSubplebbitField(post?.subplebbitAddress, (subplebbit) => subplebbit?.roles);
const isMobile = useIsMobile();
let comment = post;
@@ -42,9 +44,9 @@ export const Post = ({ post, showAllReplies = false, showReplies = true }: PostP
<div className={styles.thread}>
<div className={styles.postContainer}>
{isMobile ? (
<PostMobile post={comment} roles={subplebbit?.roles} showAllReplies={showAllReplies} showReplies={showReplies} />
<PostMobile post={comment} roles={roles} showAllReplies={showAllReplies} showReplies={showReplies} />
) : (
<PostDesktop post={comment} roles={subplebbit?.roles} showAllReplies={showAllReplies} showReplies={showReplies} />
<PostDesktop post={comment} roles={roles} showAllReplies={showAllReplies} showReplies={showReplies} />
)}
</div>
</div>