mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
feat(navigator): reactive favourites via fine-grained store (P3)
Move favourite room ids out of the useBetween navigator store into a dedicated Zustand store. useNavigatorFavourite(roomId) subscribes only to s.ids.has(roomId) (a boolean), so a FavouriteChangedEvent for one room no longer re-renders every favourite-aware view. apply() returns the same state reference when membership is unchanged.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
export { useNavigatorData } from './useNavigatorData';
|
||||
export { useNavigatorFavourite } from './useNavigatorFavourite';
|
||||
export { useNavigatorSearch } from './useNavigatorSearch';
|
||||
export { useNavigatorUiState } from './useNavigatorUiState';
|
||||
export { useNavigatorUiStore } from './navigatorUiStore';
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { useNavigatorFavouritesStore } from './navigatorFavouritesStore';
|
||||
|
||||
const reset = () => useNavigatorFavouritesStore.setState({ ids: new Set<number>() });
|
||||
|
||||
describe('navigatorFavouritesStore', () =>
|
||||
{
|
||||
beforeEach(reset);
|
||||
|
||||
it('setAll replaces membership and coerces ids to numbers', () =>
|
||||
{
|
||||
useNavigatorFavouritesStore.getState().setAll([ '1', 2, '3' ] as any);
|
||||
const { ids } = useNavigatorFavouritesStore.getState();
|
||||
expect(ids.has(1)).toBe(true);
|
||||
expect(ids.has(2)).toBe(true);
|
||||
expect(ids.has(3)).toBe(true);
|
||||
expect(ids.size).toBe(3);
|
||||
});
|
||||
|
||||
it('apply(true) adds and apply(false) removes', () =>
|
||||
{
|
||||
const { apply } = useNavigatorFavouritesStore.getState();
|
||||
apply(7, true);
|
||||
expect(useNavigatorFavouritesStore.getState().ids.has(7)).toBe(true);
|
||||
apply(7, false);
|
||||
expect(useNavigatorFavouritesStore.getState().ids.has(7)).toBe(false);
|
||||
});
|
||||
|
||||
it('apply returns the same state reference when nothing changes (no needless re-render)', () =>
|
||||
{
|
||||
useNavigatorFavouritesStore.getState().setAll([ 5 ]);
|
||||
const before = useNavigatorFavouritesStore.getState().ids;
|
||||
useNavigatorFavouritesStore.getState().apply(5, true); // already present
|
||||
expect(useNavigatorFavouritesStore.getState().ids).toBe(before);
|
||||
useNavigatorFavouritesStore.getState().apply(99, false); // already absent
|
||||
expect(useNavigatorFavouritesStore.getState().ids).toBe(before);
|
||||
});
|
||||
|
||||
it('apply creates a new Set reference when membership actually changes', () =>
|
||||
{
|
||||
useNavigatorFavouritesStore.getState().setAll([ 5 ]);
|
||||
const before = useNavigatorFavouritesStore.getState().ids;
|
||||
useNavigatorFavouritesStore.getState().apply(6, true);
|
||||
expect(useNavigatorFavouritesStore.getState().ids).not.toBe(before);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createNitroStore } from '../../state/createNitroStore';
|
||||
|
||||
export type NavigatorFavouritesState = {
|
||||
ids: Set<number>;
|
||||
};
|
||||
|
||||
export type NavigatorFavouritesActions = {
|
||||
setAll(roomIds: number[]): void;
|
||||
apply(roomId: number, added: boolean): void;
|
||||
};
|
||||
|
||||
export const useNavigatorFavouritesStore = createNitroStore<NavigatorFavouritesState & NavigatorFavouritesActions>()((set) => ({
|
||||
ids: new Set<number>(),
|
||||
|
||||
setAll: (roomIds) => set({ ids: new Set(roomIds.map(Number)) }),
|
||||
apply: (roomId, added) => set((s) =>
|
||||
{
|
||||
const id = Number(roomId);
|
||||
if(added ? s.ids.has(id) : !s.ids.has(id)) return s;
|
||||
const ids = new Set(s.ids);
|
||||
if(added) ids.add(id);
|
||||
else ids.delete(id);
|
||||
return { ids };
|
||||
})
|
||||
}));
|
||||
@@ -4,13 +4,13 @@ import { useNavigatorStore } from './useNavigatorStore';
|
||||
export const useNavigatorData = () =>
|
||||
{
|
||||
const {
|
||||
categories, eventCategories, favouriteRoomIds,
|
||||
categories, eventCategories,
|
||||
topLevelContext, topLevelContexts,
|
||||
navigatorSearches, navigatorData
|
||||
} = useBetween(useNavigatorStore);
|
||||
|
||||
return {
|
||||
categories, eventCategories, favouriteRoomIds,
|
||||
categories, eventCategories,
|
||||
topLevelContext, topLevelContexts,
|
||||
navigatorSearches, navigatorData
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ToggleFavoriteRoom } from '../../api';
|
||||
import { useNavigatorFavouritesStore } from './navigatorFavouritesStore';
|
||||
|
||||
export const useNavigatorFavourite = (roomId: number) =>
|
||||
{
|
||||
const isFavourite = useNavigatorFavouritesStore((s) => s.ids.has(Number(roomId)));
|
||||
|
||||
const toggle = useCallback(() => ToggleFavoriteRoom(Number(roomId), isFavourite), [ roomId, isFavourite ]);
|
||||
|
||||
return { isFavourite, toggle };
|
||||
};
|
||||
@@ -8,7 +8,7 @@ describe('navigator filter shapes (smoke)', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useNavigatorData());
|
||||
expect(Object.keys(result.current).sort()).toEqual([
|
||||
'categories', 'eventCategories', 'favouriteRoomIds',
|
||||
'categories', 'eventCategories',
|
||||
'navigatorData', 'navigatorSearches',
|
||||
'topLevelContext', 'topLevelContexts'
|
||||
].sort());
|
||||
|
||||
@@ -18,13 +18,13 @@ import { CreateRoomSession, GetConfigurationValue, INavigatorData,
|
||||
TryVisitRoom, VisitDesktop } from '../../api';
|
||||
import { useMessageEvent, useNitroEvent } from '../events';
|
||||
import { useNotification } from '../notification';
|
||||
import { useNavigatorFavouritesStore } from './navigatorFavouritesStore';
|
||||
import { useNavigatorUiStore } from './navigatorUiStore';
|
||||
|
||||
export const useNavigatorStore = () =>
|
||||
{
|
||||
const [ categories, setCategories ] = useState<NavigatorCategoryDataParser[]>(null);
|
||||
const [ eventCategories, setEventCategories ] = useState<NavigatorEventCategoryDataParser[]>(null);
|
||||
const [ favouriteRoomIds, setFavouriteRoomIds ] = useState<number[]>([]);
|
||||
const [ topLevelContext, setTopLevelContext ] = useState<NavigatorTopLevelContext>(null);
|
||||
const [ topLevelContexts, setTopLevelContexts ] = useState<NavigatorTopLevelContext[]>(null);
|
||||
const [ navigatorSearches, setNavigatorSearches ] = useState<NavigatorSavedSearch[]>(null);
|
||||
@@ -48,21 +48,13 @@ export const useNavigatorStore = () =>
|
||||
useMessageEvent<FavouritesEvent>(FavouritesEvent, useCallback(event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
const favoriteIds = (parser.favoriteRoomIds || []).map((x: any) => Number(x));
|
||||
setFavouriteRoomIds(favoriteIds);
|
||||
useNavigatorFavouritesStore.getState().setAll(parser.favoriteRoomIds || []);
|
||||
}, []));
|
||||
|
||||
useMessageEvent<FavouriteChangedEvent>(FavouriteChangedEvent, useCallback(event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
const roomId = Number(parser.flatId);
|
||||
const added = !!parser.added;
|
||||
setFavouriteRoomIds(prev =>
|
||||
{
|
||||
const ids = (prev || []).map((x: any) => Number(x));
|
||||
if(added) return ids.includes(roomId) ? ids : [ ...ids, roomId ];
|
||||
return ids.filter(id => id !== roomId);
|
||||
});
|
||||
useNavigatorFavouritesStore.getState().apply(parser.flatId, !!parser.added);
|
||||
}, []));
|
||||
|
||||
useMessageEvent<CanCreateRoomEventEvent>(CanCreateRoomEventEvent, useCallback(event =>
|
||||
@@ -280,7 +272,7 @@ export const useNavigatorStore = () =>
|
||||
}, []));
|
||||
|
||||
return {
|
||||
categories, eventCategories, favouriteRoomIds,
|
||||
categories, eventCategories,
|
||||
topLevelContext, topLevelContexts,
|
||||
navigatorSearches, navigatorData
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user