mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
perf(mod queue): reduce loading rerenders
This commit is contained in:
@@ -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
@@ -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 = {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user