perf: prevent unnecessary re-renders from RPC client state changes

This commit is contained in:
plebeius
2026-01-14 15:05:45 +01:00
parent 91cd84819d
commit 3799dccd88
8 changed files with 168 additions and 26 deletions
+66
View File
@@ -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;
};
+31
View File
@@ -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;
};
+29 -10
View File
@@ -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) => {