perf(mod queue): reduce loading rerenders

This commit is contained in:
Tommaso Casaburi
2026-04-27 14:59:24 +07:00
parent 5dc5408a15
commit abaa0d4e16
6 changed files with 138 additions and 9 deletions
+1
View File
@@ -349,6 +349,7 @@ describe('App', () => {
await renderApp('/mod/queue'); await renderApp('/mod/queue');
expect(container.querySelector('[data-testid="mod-queue-view"]')).toBeTruthy(); expect(container.querySelector('[data-testid="mod-queue-view"]')).toBeTruthy();
expect(container.querySelector('[data-testid="feed-cache-container"]')).toBeNull();
act(() => root.unmount()); act(() => root.unmount());
root = createRoot(container); root = createRoot(container);
+1 -1
View File
@@ -170,7 +170,7 @@ const BoardLayout = () => {
<DesktopBoardButtons /> <DesktopBoardButtons />
</> </>
)} )}
<FeedCacheContainer /> {!isOnModQueueRoute && <FeedCacheContainer />}
{shouldRenderOutlet && <Outlet />} {shouldRenderOutlet && <Outlet />}
</div> </div>
); );
@@ -23,11 +23,45 @@ const testState = vi.hoisted(() => ({
communitySnapshot: undefined as unknown, communitySnapshot: undefined as unknown,
})); }));
const accountsStoreSelectorCache = vi.hoisted(() => ({
hasValue: false,
value: undefined as unknown,
}));
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
useAccount: () => testState.account, useAccount: () => testState.account,
useAccountCommunities: () => ({ accountCommunities: testState.accountCommunities }), useAccountCommunities: () => ({ accountCommunities: testState.accountCommunities }),
})); }));
vi.mock('@bitsocial/bitsocial-react-hooks/dist/stores/accounts/index.js', () => ({
default: (
selector: (state: { activeAccountId?: string; accounts: Record<string, { communities?: typeof testState.accountCommunities }> }) => unknown,
equalityFn?: (previous: unknown, next: unknown) => boolean,
) => {
const nextValue = selector({
activeAccountId: 'active',
accounts: {
active: {
communities: testState.accountCommunities,
},
},
});
if (accountsStoreSelectorCache.hasValue && equalityFn?.(accountsStoreSelectorCache.value, nextValue)) {
return accountsStoreSelectorCache.value;
}
accountsStoreSelectorCache.hasValue = true;
accountsStoreSelectorCache.value = nextValue;
return nextValue;
},
}));
vi.mock('@bitsocial/bitsocial-react-hooks/dist/lib/community-address.js', () => ({
getEquivalentCommunityAddressGroupKey: (address: string) => (address.endsWith('.eth') ? address.slice(0, -4) + '.bso' : address),
pickPreferredEquivalentCommunityAddress: (addresses: string[]) => addresses.find((address) => address.endsWith('.bso')) || addresses[0],
}));
vi.mock('@bitsocial/bitsocial-react-hooks/dist/lib/utils', () => ({ vi.mock('@bitsocial/bitsocial-react-hooks/dist/lib/utils', () => ({
flattenCommentsPages: () => testState.flattenedReplies, flattenCommentsPages: () => testState.flattenedReplies,
})); }));
@@ -60,6 +94,14 @@ const renderHookValue = (useValue: () => unknown) => {
return latestValue; return latestValue;
}; };
const rerenderHookValue = (useValue: () => unknown) => {
act(() => {
root.render(createElement(HookHarness, { useValue }));
});
return latestValue;
};
describe('selector hooks', () => { describe('selector hooks', () => {
beforeEach(() => { beforeEach(() => {
latestValue = undefined; latestValue = undefined;
@@ -72,6 +114,8 @@ describe('selector hooks', () => {
testState.directoryLookup = {}; testState.directoryLookup = {};
testState.flattenedReplies = []; testState.flattenedReplies = [];
testState.communitySnapshot = undefined; testState.communitySnapshot = undefined;
accountsStoreSelectorCache.hasValue = false;
accountsStoreSelectorCache.value = undefined;
useAllFeedFilterStore.getState().setFilter('all'); useAllFeedFilterStore.getState().setFilter('all');
container = document.createElement('div'); container = document.createElement('div');
@@ -97,6 +141,31 @@ describe('selector hooks', () => {
]); ]);
}); });
it('keeps account board address identity stable when cached community objects change', () => {
testState.accountCommunities = {
'music.eth': { address: 'music.eth', state: 'updating' },
'tech.eth': { address: 'tech.eth', state: 'updating' },
};
const initialAddresses = rerenderHookValue(() => useAccountCommunityAddresses());
testState.accountCommunities = {
'music.eth': { address: 'music.eth', state: 'succeeded' },
'tech.eth': { address: 'tech.eth', state: 'updating' },
};
expect(rerenderHookValue(() => useAccountCommunityAddresses())).toBe(initialAddresses);
testState.accountCommunities = {
...testState.accountCommunities,
'biz.eth': { address: 'biz.eth', state: 'updating' },
};
const addressesWithNewBoard = rerenderHookValue(() => useAccountCommunityAddresses());
expect(addressesWithNewBoard).not.toBe(initialAddresses);
expect(addressesWithNewBoard).toEqual(['biz.eth', 'music.eth', 'tech.eth']);
});
it('computes moderator privileges and whether the current account authored the comment', () => { it('computes moderator privileges and whether the current account authored the comment', () => {
testState.account = { author: { address: '0xme' } }; testState.account = { author: { address: '0xme' } };
testState.communitySnapshot = { testState.communitySnapshot = {
+43 -6
View File
@@ -1,8 +1,45 @@
import { useMemo } from 'react'; import useAccountsStore from '@bitsocial/bitsocial-react-hooks/dist/stores/accounts/index.js';
import { useAccountCommunities } from '@bitsocial/bitsocial-react-hooks'; import { getEquivalentCommunityAddressGroupKey, pickPreferredEquivalentCommunityAddress } from '@bitsocial/bitsocial-react-hooks/dist/lib/community-address.js';
export const useAccountCommunityAddresses = (): string[] => { type AccountWithCommunities = {
const { accountCommunities } = useAccountCommunities({ onlyIfCached: true }); communities?: Record<string, unknown>;
return useMemo(() => Object.keys(accountCommunities), [accountCommunities]);
}; };
type AccountsStoreState = {
activeAccountId?: string;
accounts: Record<string, AccountWithCommunities | undefined>;
};
const EMPTY_ACCOUNT_COMMUNITY_ADDRESSES: string[] = [];
const areStringArraysEqual = (previous: string[], next: string[]) => {
if (previous === next) {
return true;
}
if (previous.length !== next.length) {
return false;
}
return previous.every((value, index) => value === next[index]);
};
const getAccountCommunityAddresses = (state: AccountsStoreState): string[] => {
const accountCommunities = state.activeAccountId ? state.accounts[state.activeAccountId]?.communities : undefined;
if (!accountCommunities) {
return EMPTY_ACCOUNT_COMMUNITY_ADDRESSES;
}
const groupedAddresses = new Map<string, string[]>();
for (const communityAddress of Object.keys(accountCommunities)) {
const groupKey = getEquivalentCommunityAddressGroupKey(communityAddress);
const addresses = groupedAddresses.get(groupKey);
if (addresses) {
addresses.push(communityAddress);
} else {
groupedAddresses.set(groupKey, [communityAddress]);
}
}
return [...groupedAddresses.values()].map((addresses) => pickPreferredEquivalentCommunityAddress(addresses)).sort();
};
export const useAccountCommunityAddresses = (): string[] => useAccountsStore(getAccountCommunityAddresses, areStringArraysEqual);
@@ -38,7 +38,7 @@ describe('interaction stores', () => {
useCreateBoardModalStore.getState().closeCreateBoardModal(); useCreateBoardModalStore.getState().closeCreateBoardModal();
useDirectoryModalStore.getState().closeDirectoryModal(); useDirectoryModalStore.getState().closeDirectoryModal();
useDisclaimerModalStore.getState().closeDisclaimerModal(); useDisclaimerModalStore.getState().closeDisclaimerModal();
useFeedResetStore.setState({ reset: null }); useFeedResetStore.setState({ currentResetFunction: null, reset: null });
usePostNumberStore.setState({ numberToCid: {}, cidToNumber: {} }); usePostNumberStore.setState({ numberToCid: {}, cidToNumber: {} });
useSelectedTextStore.getState().resetSelectedText(); useSelectedTextStore.getState().resetSelectedText();
useSortingStore.getState().setSortType('active'); useSortingStore.getState().setSortType('active');
@@ -80,9 +80,17 @@ describe('interaction stores', () => {
const resetMock = vi.fn(); const resetMock = vi.fn();
useFeedResetStore.getState().setResetFunction(resetMock); useFeedResetStore.getState().setResetFunction(resetMock);
const firstResetInvoker = useFeedResetStore.getState().reset;
useFeedResetStore.getState().reset?.(); useFeedResetStore.getState().reset?.();
expect(resetMock).toHaveBeenCalledTimes(1); expect(resetMock).toHaveBeenCalledTimes(1);
const nextResetMock = vi.fn();
useFeedResetStore.getState().setResetFunction(nextResetMock);
expect(useFeedResetStore.getState().reset).toBe(firstResetInvoker);
useFeedResetStore.getState().reset?.();
expect(resetMock).toHaveBeenCalledTimes(1);
expect(nextResetMock).toHaveBeenCalledTimes(1);
expect(useSortingStore.getState().sortType).toBe('active'); expect(useSortingStore.getState().sortType).toBe('active');
useSortingStore.getState().setSortType('replyCount'); useSortingStore.getState().setSortType('replyCount');
expect(useSortingStore.getState().sortType).toBe('replyCount'); expect(useSortingStore.getState().sortType).toBe('replyCount');
+15 -1
View File
@@ -2,12 +2,26 @@ import { create } from 'zustand';
interface FeedResetState { interface FeedResetState {
reset: (() => void) | null; reset: (() => void) | null;
currentResetFunction: (() => void) | null;
setResetFunction: (resetFunction: () => void) => void; setResetFunction: (resetFunction: () => void) => void;
} }
const invokeCurrentResetFunction = () => {
useFeedResetStore.getState().currentResetFunction?.();
};
const useFeedResetStore = create<FeedResetState>((set) => ({ const useFeedResetStore = create<FeedResetState>((set) => ({
reset: null, reset: null,
setResetFunction: (resetFunction) => set({ reset: resetFunction }), currentResetFunction: null,
setResetFunction: (resetFunction) =>
set((state) =>
state.currentResetFunction === resetFunction && state.reset
? state
: {
currentResetFunction: resetFunction,
reset: state.reset ?? invokeCurrentResetFunction,
},
),
})); }));
export default useFeedResetStore; export default useFeedResetStore;