Skip to content

Commit 955ff0c

Browse files
committed
refactor: optimize user fetching in MemoCommentMessage and MemoReactionListView components
1 parent 115d1ba commit 955ff0c

File tree

4 files changed

+69
-36
lines changed

4 files changed

+69
-36
lines changed

web/src/components/Inbox/MemoCommentMessage.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import { activityServiceClient, memoServiceClient, userServiceClient } from "@/c
88
import { activityNamePrefix } from "@/helpers/resource-names";
99
import useAsyncEffect from "@/hooks/useAsyncEffect";
1010
import useNavigateTo from "@/hooks/useNavigateTo";
11+
import { useUser } from "@/hooks/useUserQueries";
1112
import { handleError } from "@/lib/error";
1213
import { cn } from "@/lib/utils";
1314
import { Memo } from "@/types/proto/api/v1/memo_service_pb";
14-
import { User, UserNotification, UserNotification_Status } from "@/types/proto/api/v1/user_service_pb";
15+
import { UserNotification, UserNotification_Status } from "@/types/proto/api/v1/user_service_pb";
1516
import { useTranslate } from "@/utils/i18n";
1617

1718
interface Props {
@@ -23,10 +24,12 @@ function MemoCommentMessage({ notification }: Props) {
2324
const navigateTo = useNavigateTo();
2425
const [relatedMemo, setRelatedMemo] = useState<Memo | undefined>(undefined);
2526
const [commentMemo, setCommentMemo] = useState<Memo | undefined>(undefined);
26-
const [sender, setSender] = useState<User | undefined>(undefined);
27+
const [senderName, setSenderName] = useState<string | undefined>(undefined);
2728
const [initialized, setInitialized] = useState<boolean>(false);
2829
const [hasError, setHasError] = useState<boolean>(false);
2930

31+
const { data: sender } = useUser(senderName || "", { enabled: !!senderName });
32+
3033
useAsyncEffect(async () => {
3134
if (!notification.activityId) {
3235
return;
@@ -44,16 +47,12 @@ function MemoCommentMessage({ notification }: Props) {
4447
});
4548
setRelatedMemo(memo);
4649

47-
// Fetch the comment memo
4850
const comment = await memoServiceClient.getMemo({
4951
name: memoCommentPayload.memo,
5052
});
5153
setCommentMemo(comment);
5254

53-
const sender = await userServiceClient.getUser({
54-
name: notification.sender,
55-
});
56-
setSender(sender);
55+
setSenderName(notification.sender);
5756
setInitialized(true);
5857
}
5958
} catch (error) {

web/src/components/MemoReactionListView/hooks.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,30 @@
11
import { useQueryClient } from "@tanstack/react-query";
2-
import { uniq } from "lodash-es";
3-
import { useEffect, useState } from "react";
4-
import { memoServiceClient, userServiceClient } from "@/connect";
2+
import { useMemo } from "react";
3+
import { memoServiceClient } from "@/connect";
54
import useCurrentUser from "@/hooks/useCurrentUser";
65
import { memoKeys } from "@/hooks/useMemoQueries";
6+
import { useUsersByNames } from "@/hooks/useUserQueries";
77
import type { Memo, Reaction } from "@/types/proto/api/v1/memo_service_pb";
88
import type { User } from "@/types/proto/api/v1/user_service_pb";
99

1010
export type ReactionGroup = Map<string, User[]>;
1111

1212
export const useReactionGroups = (reactions: Reaction[]): ReactionGroup => {
13-
const [reactionGroup, setReactionGroup] = useState<ReactionGroup>(new Map());
13+
const creatorNames = useMemo(() => reactions.map((r) => r.creator), [reactions]);
14+
const { data: userMap } = useUsersByNames(creatorNames);
1415

15-
useEffect(() => {
16-
const fetchReactionGroups = async () => {
17-
const newReactionGroup = new Map<string, User[]>();
18-
for (const reaction of reactions) {
19-
// Fetch user via gRPC directly since we need it within an effect
20-
const user = await userServiceClient.getUser({ name: reaction.creator });
21-
const users = newReactionGroup.get(reaction.reactionType) || [];
22-
users.push(user);
23-
newReactionGroup.set(reaction.reactionType, uniq(users));
24-
}
25-
setReactionGroup(newReactionGroup);
26-
};
27-
fetchReactionGroups();
28-
}, [reactions]);
16+
return useMemo(() => {
17+
const reactionGroup = new Map<string, User[]>();
18+
for (const reaction of reactions) {
19+
const user = userMap?.get(reaction.creator);
20+
if (!user) continue;
2921

30-
return reactionGroup;
22+
const users = reactionGroup.get(reaction.reactionType) || [];
23+
users.push(user);
24+
reactionGroup.set(reaction.reactionType, users);
25+
}
26+
return reactionGroup;
27+
}, [reactions, userMap]);
3128
};
3229

3330
interface UseReactionActionsOptions {

web/src/components/PagedMemoList/PagedMemoList.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { ArrowUpIcon, LoaderIcon } from "lucide-react";
1+
import { useQueryClient } from "@tanstack/react-query";
2+
import { ArrowUpIcon } from "lucide-react";
23
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
34
import { matchPath } from "react-router-dom";
45
import { Button } from "@/components/ui/button";
56
import { userServiceClient } from "@/connect";
67
import { useView } from "@/contexts/ViewContext";
78
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
89
import { useInfiniteMemos } from "@/hooks/useMemoQueries";
10+
import { userKeys } from "@/hooks/useUserQueries";
911
import { Routes } from "@/router";
1012
import { State } from "@/types/proto/api/v1/common_pb";
1113
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
@@ -81,6 +83,7 @@ function useAutoFetchWhenNotScrollable({
8183
const PagedMemoList = (props: Props) => {
8284
const t = useTranslate();
8385
const { layout } = useView();
86+
const queryClient = useQueryClient();
8487

8588
// Show memo editor only on the root route
8689
const showMemoEditor = Boolean(matchPath(Routes.ROOT, window.location.pathname));
@@ -99,22 +102,25 @@ const PagedMemoList = (props: Props) => {
99102
// Apply custom sorting if provided, otherwise use memos directly
100103
const sortedMemoList = useMemo(() => (props.listSort ? props.listSort(memos) : memos), [memos, props.listSort]);
101104

102-
// Batch-fetch creators when new data arrives to improve performance
105+
// Prefetch creators when new data arrives to improve performance
103106
useEffect(() => {
104107
if (!data?.pages || !props.showCreator) return;
105108

106109
const lastPage = data.pages[data.pages.length - 1];
107110
if (!lastPage?.memos) return;
108111

109112
const uniqueCreators = Array.from(new Set(lastPage.memos.map((memo) => memo.creator)));
110-
void Promise.allSettled(
111-
uniqueCreators.map((creator) =>
112-
userServiceClient.getUser({ name: creator }).catch(() => {
113-
/* silently ignore errors */
114-
}),
115-
),
116-
);
117-
}, [data?.pages, props.showCreator]);
113+
for (const creator of uniqueCreators) {
114+
void queryClient.prefetchQuery({
115+
queryKey: userKeys.detail(creator),
116+
queryFn: async () => {
117+
const user = await userServiceClient.getUser({ name: creator });
118+
return user;
119+
},
120+
staleTime: 1000 * 60 * 5,
121+
});
122+
}
123+
}, [data?.pages, props.showCreator, queryClient]);
118124

119125
// Auto-fetch hook: fetches more content when page isn't scrollable
120126
useAutoFetchWhenNotScrollable({

web/src/hooks/useUserQueries.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const userKeys = {
1515
currentUser: () => [...userKeys.all, "current"] as const,
1616
shortcuts: () => [...userKeys.all, "shortcuts"] as const,
1717
notifications: () => [...userKeys.all, "notifications"] as const,
18+
byNames: (names: string[]) => [...userKeys.all, "byNames", ...names.sort()] as const,
1819
};
1920

2021
// NOTE: This hook is currently UNUSED in favor of the AuthContext-based
@@ -226,3 +227,33 @@ export function useUpdateUserGeneralSetting(currentUserName?: string) {
226227
},
227228
});
228229
}
230+
231+
// Hook to fetch multiple users by names (returns Map<name, User>)
232+
export function useUsersByNames(names: string[]) {
233+
const enabled = names.length > 0;
234+
const uniqueNames = Array.from(new Set(names));
235+
236+
return useQuery({
237+
queryKey: userKeys.byNames(uniqueNames),
238+
queryFn: async () => {
239+
const users = await Promise.all(
240+
uniqueNames.map(async (name) => {
241+
try {
242+
const user = await userServiceClient.getUser({ name });
243+
return { name, user };
244+
} catch {
245+
return { name, user: undefined };
246+
}
247+
}),
248+
);
249+
250+
const userMap = new Map<string, User | undefined>();
251+
for (const { name, user } of users) {
252+
userMap.set(name, user);
253+
}
254+
return userMap;
255+
},
256+
enabled,
257+
staleTime: 1000 * 60 * 5, // 5 minutes - user profiles don't change often
258+
});
259+
}

0 commit comments

Comments
 (0)