mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
perf: prevent unnecessary re-renders from RPC client state changes
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useAccountComment, useSubplebbitStats } from '@plebbit/plebbit-react-hooks';
|
||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
||||
import { useStableSubplebbitStats } from '../../hooks/use-stable-subplebbit-stats';
|
||||
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
|
||||
import useSubplebbitStatsVisibilityStore from '../../stores/use-subplebbit-stats-visibility-store';
|
||||
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||
@@ -19,7 +20,9 @@ const SubplebbitStats = () => {
|
||||
const address = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.address);
|
||||
const createdAt = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.createdAt);
|
||||
|
||||
let stats = useSubplebbitStats({ subplebbitAddress: address });
|
||||
// Use stable stats hook that doesn't depend on useSubplebbit internally
|
||||
const stats = useStableSubplebbitStats(address);
|
||||
|
||||
const { hiddenStats, toggleVisibility } = useSubplebbitStatsVisibilityStore();
|
||||
const isHidden = hiddenStats[address];
|
||||
|
||||
@@ -51,13 +54,13 @@ const SubplebbitStats = () => {
|
||||
<td>
|
||||
<Trans
|
||||
i18nKey='board_stats_hour'
|
||||
values={{ userCount: stats.hourActiveUserCount ?? '?', postCount: stats.hourPostCount ?? '?' }}
|
||||
values={{ userCount: stats?.hourActiveUserCount ?? '?', postCount: stats?.hourPostCount ?? '?' }}
|
||||
components={{ 1: <span key='hour-stat-value' className={styles.statValue} /> }}
|
||||
/>
|
||||
{' / '}
|
||||
<Trans
|
||||
i18nKey='board_stats_day'
|
||||
values={{ userCount: stats.dayActiveUserCount ?? '?', postCount: stats.dayPostCount ?? '?' }}
|
||||
values={{ userCount: stats?.dayActiveUserCount ?? '?', postCount: stats?.dayPostCount ?? '?' }}
|
||||
components={{ 1: <span key='day-stat-value' className={styles.statValue} /> }}
|
||||
/>
|
||||
</td>
|
||||
@@ -66,13 +69,13 @@ const SubplebbitStats = () => {
|
||||
<td>
|
||||
<Trans
|
||||
i18nKey='board_stats_week'
|
||||
values={{ userCount: stats.weekActiveUserCount ?? '?', postCount: stats.weekPostCount ?? '?' }}
|
||||
values={{ userCount: stats?.weekActiveUserCount ?? '?', postCount: stats?.weekPostCount ?? '?' }}
|
||||
components={{ 1: <span key='week-stat-value' className={styles.statValue} /> }}
|
||||
/>
|
||||
{' / '}
|
||||
<Trans
|
||||
i18nKey='board_stats_month'
|
||||
values={{ userCount: stats.monthActiveUserCount ?? '?', postCount: stats.monthPostCount ?? '?' }}
|
||||
values={{ userCount: stats?.monthActiveUserCount ?? '?', postCount: stats?.monthPostCount ?? '?' }}
|
||||
components={{ 1: <span key='month-stat-value' className={styles.statValue} /> }}
|
||||
/>
|
||||
</td>
|
||||
@@ -83,7 +86,7 @@ const SubplebbitStats = () => {
|
||||
{' / '}
|
||||
<Trans
|
||||
i18nKey='board_stats_all'
|
||||
values={{ userCount: stats.allActiveUserCount ?? '?', postCount: stats.allPostCount ?? '?' }}
|
||||
values={{ userCount: stats?.allActiveUserCount ?? '?', postCount: stats?.allPostCount ?? '?' }}
|
||||
components={{ 1: <span key='all-stat-value' className={styles.statValue} /> }}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import { create } from 'zustand';
|
||||
import { useSubplebbitField } from './use-stable-subplebbit';
|
||||
|
||||
// Store to cache fetched stats and track pending fetches
|
||||
interface SubplebbitStatsState {
|
||||
stats: { [address: string]: any };
|
||||
pendingCids: { [cid: string]: boolean };
|
||||
setStats: (address: string, stats: any) => void;
|
||||
setPending: (cid: string, pending: boolean) => void;
|
||||
}
|
||||
|
||||
const useStableSubplebbitStatsStore = create<SubplebbitStatsState>((set) => ({
|
||||
stats: {},
|
||||
pendingCids: {},
|
||||
setStats: (address, stats) =>
|
||||
set((state) => ({
|
||||
stats: { ...state.stats, [address]: stats },
|
||||
})),
|
||||
setPending: (cid, pending) =>
|
||||
set((state) => ({
|
||||
pendingCids: { ...state.pendingCids, [cid]: pending },
|
||||
})),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Stable version of useSubplebbitStats that doesn't depend on useSubplebbit internally.
|
||||
* Uses useSubplebbitField to get the statsCid without re-rendering on updatingState changes.
|
||||
*/
|
||||
export const useStableSubplebbitStats = (subplebbitAddress: string | undefined) => {
|
||||
const account = useAccount();
|
||||
|
||||
// Get statsCid using stable field selector - won't re-render on updatingState changes
|
||||
const statsCid = useSubplebbitField(subplebbitAddress, (sub) => sub?.statsCid);
|
||||
|
||||
const stats = useStableSubplebbitStatsStore((state) => (subplebbitAddress ? state.stats[subplebbitAddress] : undefined));
|
||||
const pendingCids = useStableSubplebbitStatsStore((state) => state.pendingCids);
|
||||
const setStats = useStableSubplebbitStatsStore((state) => state.setStats);
|
||||
const setPending = useStableSubplebbitStatsStore((state) => state.setPending);
|
||||
|
||||
useEffect(() => {
|
||||
if (!subplebbitAddress || !statsCid || !account) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't fetch if already fetched or pending
|
||||
if (stats || pendingCids[statsCid]) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPending(statsCid, true);
|
||||
|
||||
account.plebbit
|
||||
.fetchCid(statsCid)
|
||||
.then((fetchedStats: any) => {
|
||||
setStats(subplebbitAddress, JSON.parse(fetchedStats));
|
||||
})
|
||||
.catch((error: any) => {
|
||||
setPending(statsCid, false);
|
||||
console.error('useStableSubplebbitStats fetchCid error', { subplebbitAddress, statsCid, error });
|
||||
});
|
||||
}, [subplebbitAddress, statsCid, account, stats, pendingCids, setStats, setPending]);
|
||||
|
||||
return stats;
|
||||
};
|
||||
@@ -69,3 +69,34 @@ export const useSubplebbitField = <T>(subplebbitAddress: string | undefined, sel
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get multiple subplebbits with stable references.
|
||||
* Only re-renders when actual content changes (updatedAt, posts pages), not transient state.
|
||||
*
|
||||
* @param subplebbitAddresses - Array of subplebbit addresses
|
||||
* @returns Array of subplebbit objects
|
||||
*/
|
||||
export const useStableSubplebbits = (subplebbitAddresses: string[]) => {
|
||||
const subplebbits = useSubplebbitsStore(
|
||||
(state) => subplebbitAddresses.map((address) => state.subplebbits[address]),
|
||||
// Custom equality: only re-render if stable content fields change
|
||||
(prev, next) => {
|
||||
if (prev.length !== next.length) return false;
|
||||
return prev.every((p, i) => {
|
||||
const n = next[i];
|
||||
if (p === n) return true;
|
||||
if (!p || !n) return p === n;
|
||||
// Compare stable content fields only - ignore updatingState, state, clients, etc.
|
||||
return (
|
||||
p.address === n.address &&
|
||||
p.updatedAt === n.updatedAt &&
|
||||
// For PopularThreadsBox: compare posts pages reference (changes when new posts loaded)
|
||||
p.posts?.pages?.hot === n.posts?.pages?.hot
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return subplebbits;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useAccount, useSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||
import { useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import { create } from 'zustand';
|
||||
|
||||
const pendingFetchCid: { [cid: string]: boolean } = {};
|
||||
|
||||
/**
|
||||
* Hook to get stats for multiple subplebbits.
|
||||
* Uses stable store selector to avoid re-renders from updatingState changes.
|
||||
*/
|
||||
const useSubplebbitsStats = (options: any) => {
|
||||
const { subplebbitAddresses, accountName } = options || {};
|
||||
const account = useAccount({ accountName });
|
||||
const { subplebbits } = useSubplebbits({ subplebbitAddresses });
|
||||
|
||||
// Use stable selector to only get statsCid for each address
|
||||
// This avoids re-renders when only updatingState changes
|
||||
const statsCids = useSubplebbitsStore(
|
||||
(state) =>
|
||||
(subplebbitAddresses || []).map((address: string) => ({
|
||||
address,
|
||||
statsCid: state.subplebbits[address]?.statsCid,
|
||||
})),
|
||||
// Custom equality: only re-render if statsCid values change
|
||||
(prev, next) => {
|
||||
if (prev.length !== next.length) return false;
|
||||
return prev.every((p: any, i: number) => p.address === next[i].address && p.statsCid === next[i].statsCid);
|
||||
},
|
||||
);
|
||||
|
||||
const { setSubplebbitStats, subplebbitsStats } = useSubplebbitsStatsStore();
|
||||
|
||||
@@ -16,21 +35,21 @@ const useSubplebbitsStats = (options: any) => {
|
||||
return;
|
||||
}
|
||||
|
||||
subplebbits.forEach((subplebbit) => {
|
||||
if (subplebbit && subplebbit.statsCid && !subplebbitsStats[subplebbit.address] && !pendingFetchCid[subplebbit.statsCid]) {
|
||||
pendingFetchCid[subplebbit.statsCid] = true;
|
||||
statsCids.forEach(({ address, statsCid }: { address: string; statsCid: string | undefined }) => {
|
||||
if (statsCid && !subplebbitsStats[address] && !pendingFetchCid[statsCid]) {
|
||||
pendingFetchCid[statsCid] = true;
|
||||
account.plebbit
|
||||
.fetchCid(subplebbit.statsCid)
|
||||
.fetchCid(statsCid)
|
||||
.then((fetchedStats: any) => {
|
||||
setSubplebbitStats(subplebbit.address, JSON.parse(fetchedStats));
|
||||
setSubplebbitStats(address, JSON.parse(fetchedStats));
|
||||
})
|
||||
.catch((error: any) => {
|
||||
pendingFetchCid[subplebbit.statsCid] = false;
|
||||
console.error('Fetching subplebbit stats failed', { subplebbitAddress: subplebbit.address, error });
|
||||
pendingFetchCid[statsCid] = false;
|
||||
console.error('Fetching subplebbit stats failed', { subplebbitAddress: address, error });
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [account, subplebbits, setSubplebbitStats, subplebbitsStats, subplebbitAddresses]);
|
||||
}, [account, statsCids, setSubplebbitStats, subplebbitsStats, subplebbitAddresses]);
|
||||
|
||||
return useMemo(() => {
|
||||
return subplebbitAddresses.reduce((acc: any, address: any) => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
||||
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Comment, useAccount, useFeed, useSubplebbit, useBlock, useAccountComments } from '@plebbit/plebbit-react-hooks';
|
||||
import { Comment, useAccount, useFeed, useBlock, useAccountComments } from '@plebbit/plebbit-react-hooks';
|
||||
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import useCatalogFeedRows from '../../hooks/use-catalog-feed-rows';
|
||||
@@ -479,8 +480,14 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
}
|
||||
}, [reset, setResetFunction, isVisible]);
|
||||
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const { error, shortAddress, state, title } = subplebbit || {};
|
||||
// Use stable field selectors to avoid re-renders from updatingState changes
|
||||
const error = useSubplebbitField(subplebbitAddress, (sub) => sub?.error);
|
||||
const shortAddress = useSubplebbitField(subplebbitAddress, (sub) => sub?.shortAddress);
|
||||
const title = useSubplebbitField(subplebbitAddress, (sub) => sub?.title);
|
||||
// Derive state from updatedAt field - if updatedAt exists, state is 'succeeded'
|
||||
const updatedAt = useSubplebbitField(subplebbitAddress, (sub) => sub?.updatedAt);
|
||||
const state = updatedAt ? 'succeeded' : 'fetching-ipns';
|
||||
|
||||
const { blocked, unblock } = useBlock({ address: subplebbitAddress });
|
||||
|
||||
const feedLength = feed.length;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, FormEvent } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from './home.module.css';
|
||||
import { useDefaultSubplebbits, useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
|
||||
import useSubplebbitsStats from '../../hooks/use-subplebbits-stats';
|
||||
@@ -191,7 +190,6 @@ export const HomeLogo = () => {
|
||||
const Home = () => {
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const subplebbitAddresses = useDefaultSubplebbitAddresses();
|
||||
const { subplebbits } = useSubplebbits({ subplebbitAddresses });
|
||||
const { closeDirectoryModal } = useDirectoryModalStore();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -214,7 +212,7 @@ const Home = () => {
|
||||
<SearchBar />
|
||||
<InfoBox />
|
||||
<BoardsList multisub={defaultSubplebbits} />
|
||||
<PopularThreadsBox multisub={defaultSubplebbits} subplebbits={subplebbits} />
|
||||
<PopularThreadsBox multisub={defaultSubplebbits} subplebbitAddresses={subplebbitAddresses} />
|
||||
<Stats subplebbitAddresses={subplebbitAddresses} />
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useMemo, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Comment, Subplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Comment, Subplebbit, useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import styles from '../home.module.css';
|
||||
import usePopularPosts from '../../../hooks/use-popular-posts';
|
||||
import usePopularThreadsOptionsStore from '../../../stores/use-popular-threads-options-store';
|
||||
@@ -12,6 +13,7 @@ import BoxModal from '../box-modal';
|
||||
import { MultisubSubplebbit, useDefaultSubplebbits } from '../../../hooks/use-default-subplebbits';
|
||||
import { getBoardPath } from '../../../lib/utils/route-utils';
|
||||
import { removeMarkdown } from '../../../lib/utils/post-utils';
|
||||
import { useStableSubplebbits } from '../../../hooks/use-stable-subplebbit';
|
||||
|
||||
interface PopularThreadProps {
|
||||
post: Comment;
|
||||
@@ -67,10 +69,25 @@ const PopularThreadCard = memo(
|
||||
},
|
||||
);
|
||||
|
||||
const PopularThreadsBox = ({ multisub, subplebbits }: { multisub: MultisubSubplebbit[]; subplebbits: any }) => {
|
||||
// Uses stable subplebbits hook to avoid re-renders from updatingState changes
|
||||
const PopularThreadsBox = ({ multisub, subplebbitAddresses }: { multisub: MultisubSubplebbit[]; subplebbitAddresses: string[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const { showWorksafeContentOnly, showNsfwContentOnly } = usePopularThreadsOptionsStore();
|
||||
|
||||
const account = useAccount();
|
||||
const addSubplebbitToStore = useSubplebbitsStore((state) => state.addSubplebbitToStore);
|
||||
|
||||
// Trigger fetching subplebbits (same as useSubplebbits does internally)
|
||||
useEffect(() => {
|
||||
if (!account || !subplebbitAddresses) return;
|
||||
for (const address of subplebbitAddresses) {
|
||||
addSubplebbitToStore(address, account).catch(() => {});
|
||||
}
|
||||
}, [subplebbitAddresses?.toString(), account?.id]);
|
||||
|
||||
// Use stable hook that only re-renders when actual content changes
|
||||
const subplebbits = useStableSubplebbits(subplebbitAddresses);
|
||||
|
||||
const getFilteredSubplebbits = () => {
|
||||
if (showWorksafeContentOnly) {
|
||||
return subplebbits.filter((sub: Subplebbit) => {
|
||||
|
||||
@@ -71,6 +71,7 @@ const PostPage = () => {
|
||||
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const { error: subplebbitError, shortAddress, title } = subplebbit || {};
|
||||
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
|
||||
// if the comment is a reply, return the post comment instead, then the reply will be highlighted in the thread
|
||||
|
||||
Reference in New Issue
Block a user