diff --git a/src/hooks/use-stable-subplebbit.ts b/src/hooks/use-stable-subplebbit.ts index 416a6045..38e0db1f 100644 --- a/src/hooks/use-stable-subplebbit.ts +++ b/src/hooks/use-stable-subplebbit.ts @@ -69,34 +69,3 @@ export const useSubplebbitField = (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; -}; diff --git a/src/hooks/use-subplebbits-stats.ts b/src/hooks/use-subplebbits-stats.ts index 553dbde0..5dcee15b 100644 --- a/src/hooks/use-subplebbits-stats.ts +++ b/src/hooks/use-subplebbits-stats.ts @@ -1,70 +1,13 @@ -import { useEffect, useMemo } from 'react'; -import { useAccount } from '@plebbit/plebbit-react-hooks'; -import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; +import { useEffect } from 'react'; +import { useSubplebbitStats } from '@plebbit/plebbit-react-hooks'; 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 }); - - // 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(); - - useEffect(() => { - if (!subplebbitAddresses || subplebbitAddresses.length === 0 || !account) { - return; - } - - statsCids.forEach(({ address, statsCid }: { address: string; statsCid: string | undefined }) => { - if (statsCid && !subplebbitsStats[address] && !pendingFetchCid[statsCid]) { - pendingFetchCid[statsCid] = true; - account.plebbit - .fetchCid(statsCid) - .then((fetchedStats: any) => { - setSubplebbitStats(address, JSON.parse(fetchedStats)); - }) - .catch((error: any) => { - pendingFetchCid[statsCid] = false; - console.error('Fetching subplebbit stats failed', { subplebbitAddress: address, error }); - }); - } - }); - }, [account, statsCids, setSubplebbitStats, subplebbitsStats, subplebbitAddresses]); - - return useMemo(() => { - return subplebbitAddresses.reduce((acc: any, address: any) => { - acc[address] = subplebbitsStats[address] || { loading: true }; - return acc; - }, {}); - }, [subplebbitsStats, subplebbitAddresses]); -}; - export type SubplebbitsStatsState = { subplebbitsStats: { [subplebbitAddress: string]: any }; - setSubplebbitStats: Function; + setSubplebbitStats: (subplebbitAddress: string, stats: any) => void; }; -const useSubplebbitsStatsStore = create((set) => ({ +export const useSubplebbitsStatsStore = create((set) => ({ subplebbitsStats: {}, setSubplebbitStats: (subplebbitAddress: string, subplebbitStats: any) => set((state) => ({ @@ -72,4 +15,20 @@ const useSubplebbitsStatsStore = create((set) => ({ })), })); -export default useSubplebbitsStats; +/** + * Component that fetches stats for a single subplebbit and stores them. + * Render one of these for each subplebbit you want to track stats for. + */ +export const SubplebbitStatsCollector = ({ subplebbitAddress }: { subplebbitAddress: string }) => { + const stats = useSubplebbitStats({ subplebbitAddress }); + const setSubplebbitStats = useSubplebbitsStatsStore((state) => state.setSubplebbitStats); + + useEffect(() => { + // Only update store when we have actual stats (not just loading state) + if (stats && stats.allPostCount !== undefined) { + setSubplebbitStats(subplebbitAddress, stats); + } + }, [stats, subplebbitAddress, setSubplebbitStats]); + + return null; // This is a data-fetching component, renders nothing +}; diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx index 6cc3884d..6b65d5a9 100644 --- a/src/views/catalog/catalog.tsx +++ b/src/views/catalog/catalog.tsx @@ -1,8 +1,7 @@ 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, useBlock, useAccountComments } from '@plebbit/plebbit-react-hooks'; -import { useSubplebbitField } from '../../hooks/use-stable-subplebbit'; +import { Comment, useAccount, useFeed, useSubplebbit, useBlock, useAccountComments } from '@plebbit/plebbit-react-hooks'; import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso'; import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils'; import useCatalogFeedRows from '../../hooks/use-catalog-feed-rows'; @@ -480,14 +479,8 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, } }, [reset, setResetFunction, isVisible]); - // 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 subplebbit = useSubplebbit({ subplebbitAddress }); + const { error, shortAddress, state, title } = subplebbit || {}; const { blocked, unblock } = useBlock({ address: subplebbitAddress }); const feedLength = feed.length; diff --git a/src/views/home/home.tsx b/src/views/home/home.tsx index 85b093d0..3f13a593 100644 --- a/src/views/home/home.tsx +++ b/src/views/home/home.tsx @@ -1,9 +1,10 @@ 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'; +import { SubplebbitStatsCollector, useSubplebbitsStatsStore } from '../../hooks/use-subplebbits-stats'; import PopularThreadsBox from './popular-threads-box'; import BoardsList from './boards-list'; import Version from '../../components/version'; @@ -74,45 +75,48 @@ const InfoBox = () => { const Stats = ({ subplebbitAddresses }: { subplebbitAddresses: string[] }) => { const { t } = useTranslation(); - const stats = useSubplebbitsStats({ subplebbitAddresses }); + const subplebbitsStats = useSubplebbitsStatsStore((state) => state.subplebbitsStats); - const allStatsLoaded = useMemo(() => { - return subplebbitAddresses.every((address) => stats[address]); - }, [stats, subplebbitAddresses]); - - const { totalPosts, currentUsers } = useMemo(() => { + const { totalPosts, currentUsers, boardsTracked } = useMemo(() => { let totalPosts = 0; let currentUsers = 0; + let boardsTracked = 0; - if (allStatsLoaded) { - Object.values(stats).forEach((stat: any) => { + subplebbitAddresses.forEach((address) => { + const stat = subplebbitsStats[address]; + if (stat) { totalPosts += stat.allPostCount || 0; currentUsers += stat.weekActiveUserCount || 0; - }); - } + boardsTracked++; + } + }); - return { totalPosts, currentUsers }; - }, [stats, allStatsLoaded]); - - const boardsTracked = Object.values(stats).filter((stat: any) => stat && !stat.loading).length; + return { totalPosts, currentUsers, boardsTracked }; + }, [subplebbitsStats, subplebbitAddresses]); return ( -
-
-

{t('stats')}

-
-
-
- {t('total_posts')} {totalPosts} + <> + {/* Render collectors to fetch stats for each subplebbit */} + {subplebbitAddresses.map((address) => ( + + ))} +
+
+

{t('stats')}

-
- {t('current_users')} {currentUsers} -
-
- {t('boards_tracked')} {boardsTracked} +
+
+ {t('total_posts')} {totalPosts} +
+
+ {t('current_users')} {currentUsers} +
+
+ {t('boards_tracked')} {boardsTracked} +
-
+ ); }; @@ -190,6 +194,7 @@ export const HomeLogo = () => { const Home = () => { const defaultSubplebbits = useDefaultSubplebbits(); const subplebbitAddresses = useDefaultSubplebbitAddresses(); + const { subplebbits } = useSubplebbits({ subplebbitAddresses }); const { closeDirectoryModal } = useDirectoryModalStore(); useEffect(() => { @@ -212,7 +217,7 @@ const Home = () => { - +
diff --git a/src/views/home/popular-threads-box/popular-threads-box.tsx b/src/views/home/popular-threads-box/popular-threads-box.tsx index a7a26e9d..fd6cb0bf 100644 --- a/src/views/home/popular-threads-box/popular-threads-box.tsx +++ b/src/views/home/popular-threads-box/popular-threads-box.tsx @@ -1,8 +1,7 @@ -import { memo, useMemo, useEffect } from 'react'; +import { memo, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Comment, Subplebbit, useAccount } from '@plebbit/plebbit-react-hooks'; -import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; +import { Comment, Subplebbit } from '@plebbit/plebbit-react-hooks'; import styles from '../home.module.css'; import usePopularPosts from '../../../hooks/use-popular-posts'; import usePopularThreadsOptionsStore from '../../../stores/use-popular-threads-options-store'; @@ -13,7 +12,6 @@ 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; @@ -69,25 +67,10 @@ const PopularThreadCard = memo( }, ); -// Uses stable subplebbits hook to avoid re-renders from updatingState changes -const PopularThreadsBox = ({ multisub, subplebbitAddresses }: { multisub: MultisubSubplebbit[]; subplebbitAddresses: string[] }) => { +const PopularThreadsBox = ({ multisub, subplebbits }: { multisub: MultisubSubplebbit[]; subplebbits: any }) => { 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) => { diff --git a/src/views/post/post.tsx b/src/views/post/post.tsx index 20bc8e9e..a4edb3e4 100644 --- a/src/views/post/post.tsx +++ b/src/views/post/post.tsx @@ -71,7 +71,6 @@ 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