refactor: optimize user fetching in MemoCommentMessage and MemoReactionListView components

This commit is contained in:
Johnny 2025-12-28 17:50:43 +08:00
parent 115d1bacd7
commit 955ff0cad6
4 changed files with 69 additions and 36 deletions

View File

@ -8,10 +8,11 @@ import { activityServiceClient, memoServiceClient, userServiceClient } from "@/c
import { activityNamePrefix } from "@/helpers/resource-names";
import useAsyncEffect from "@/hooks/useAsyncEffect";
import useNavigateTo from "@/hooks/useNavigateTo";
import { useUser } from "@/hooks/useUserQueries";
import { handleError } from "@/lib/error";
import { cn } from "@/lib/utils";
import { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { User, UserNotification, UserNotification_Status } from "@/types/proto/api/v1/user_service_pb";
import { UserNotification, UserNotification_Status } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
interface Props {
@ -23,10 +24,12 @@ function MemoCommentMessage({ notification }: Props) {
const navigateTo = useNavigateTo();
const [relatedMemo, setRelatedMemo] = useState<Memo | undefined>(undefined);
const [commentMemo, setCommentMemo] = useState<Memo | undefined>(undefined);
const [sender, setSender] = useState<User | undefined>(undefined);
const [senderName, setSenderName] = useState<string | undefined>(undefined);
const [initialized, setInitialized] = useState<boolean>(false);
const [hasError, setHasError] = useState<boolean>(false);
const { data: sender } = useUser(senderName || "", { enabled: !!senderName });
useAsyncEffect(async () => {
if (!notification.activityId) {
return;
@ -44,16 +47,12 @@ function MemoCommentMessage({ notification }: Props) {
});
setRelatedMemo(memo);
// Fetch the comment memo
const comment = await memoServiceClient.getMemo({
name: memoCommentPayload.memo,
});
setCommentMemo(comment);
const sender = await userServiceClient.getUser({
name: notification.sender,
});
setSender(sender);
setSenderName(notification.sender);
setInitialized(true);
}
} catch (error) {

View File

@ -1,33 +1,30 @@
import { useQueryClient } from "@tanstack/react-query";
import { uniq } from "lodash-es";
import { useEffect, useState } from "react";
import { memoServiceClient, userServiceClient } from "@/connect";
import { useMemo } from "react";
import { memoServiceClient } from "@/connect";
import useCurrentUser from "@/hooks/useCurrentUser";
import { memoKeys } from "@/hooks/useMemoQueries";
import { useUsersByNames } from "@/hooks/useUserQueries";
import type { Memo, Reaction } from "@/types/proto/api/v1/memo_service_pb";
import type { User } from "@/types/proto/api/v1/user_service_pb";
export type ReactionGroup = Map<string, User[]>;
export const useReactionGroups = (reactions: Reaction[]): ReactionGroup => {
const [reactionGroup, setReactionGroup] = useState<ReactionGroup>(new Map());
const creatorNames = useMemo(() => reactions.map((r) => r.creator), [reactions]);
const { data: userMap } = useUsersByNames(creatorNames);
useEffect(() => {
const fetchReactionGroups = async () => {
const newReactionGroup = new Map<string, User[]>();
for (const reaction of reactions) {
// Fetch user via gRPC directly since we need it within an effect
const user = await userServiceClient.getUser({ name: reaction.creator });
const users = newReactionGroup.get(reaction.reactionType) || [];
users.push(user);
newReactionGroup.set(reaction.reactionType, uniq(users));
}
setReactionGroup(newReactionGroup);
};
fetchReactionGroups();
}, [reactions]);
return useMemo(() => {
const reactionGroup = new Map<string, User[]>();
for (const reaction of reactions) {
const user = userMap?.get(reaction.creator);
if (!user) continue;
return reactionGroup;
const users = reactionGroup.get(reaction.reactionType) || [];
users.push(user);
reactionGroup.set(reaction.reactionType, users);
}
return reactionGroup;
}, [reactions, userMap]);
};
interface UseReactionActionsOptions {

View File

@ -1,4 +1,5 @@
import { ArrowUpIcon, LoaderIcon } from "lucide-react";
import { useQueryClient } from "@tanstack/react-query";
import { ArrowUpIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { matchPath } from "react-router-dom";
import { Button } from "@/components/ui/button";
@ -6,6 +7,7 @@ import { userServiceClient } from "@/connect";
import { useView } from "@/contexts/ViewContext";
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
import { useInfiniteMemos } from "@/hooks/useMemoQueries";
import { userKeys } from "@/hooks/useUserQueries";
import { Routes } from "@/router";
import { State } from "@/types/proto/api/v1/common_pb";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
@ -81,6 +83,7 @@ function useAutoFetchWhenNotScrollable({
const PagedMemoList = (props: Props) => {
const t = useTranslate();
const { layout } = useView();
const queryClient = useQueryClient();
// Show memo editor only on the root route
const showMemoEditor = Boolean(matchPath(Routes.ROOT, window.location.pathname));
@ -99,7 +102,7 @@ const PagedMemoList = (props: Props) => {
// Apply custom sorting if provided, otherwise use memos directly
const sortedMemoList = useMemo(() => (props.listSort ? props.listSort(memos) : memos), [memos, props.listSort]);
// Batch-fetch creators when new data arrives to improve performance
// Prefetch creators when new data arrives to improve performance
useEffect(() => {
if (!data?.pages || !props.showCreator) return;
@ -107,14 +110,17 @@ const PagedMemoList = (props: Props) => {
if (!lastPage?.memos) return;
const uniqueCreators = Array.from(new Set(lastPage.memos.map((memo) => memo.creator)));
void Promise.allSettled(
uniqueCreators.map((creator) =>
userServiceClient.getUser({ name: creator }).catch(() => {
/* silently ignore errors */
}),
),
);
}, [data?.pages, props.showCreator]);
for (const creator of uniqueCreators) {
void queryClient.prefetchQuery({
queryKey: userKeys.detail(creator),
queryFn: async () => {
const user = await userServiceClient.getUser({ name: creator });
return user;
},
staleTime: 1000 * 60 * 5,
});
}
}, [data?.pages, props.showCreator, queryClient]);
// Auto-fetch hook: fetches more content when page isn't scrollable
useAutoFetchWhenNotScrollable({

View File

@ -15,6 +15,7 @@ export const userKeys = {
currentUser: () => [...userKeys.all, "current"] as const,
shortcuts: () => [...userKeys.all, "shortcuts"] as const,
notifications: () => [...userKeys.all, "notifications"] as const,
byNames: (names: string[]) => [...userKeys.all, "byNames", ...names.sort()] as const,
};
// NOTE: This hook is currently UNUSED in favor of the AuthContext-based
@ -226,3 +227,33 @@ export function useUpdateUserGeneralSetting(currentUserName?: string) {
},
});
}
// Hook to fetch multiple users by names (returns Map<name, User>)
export function useUsersByNames(names: string[]) {
const enabled = names.length > 0;
const uniqueNames = Array.from(new Set(names));
return useQuery({
queryKey: userKeys.byNames(uniqueNames),
queryFn: async () => {
const users = await Promise.all(
uniqueNames.map(async (name) => {
try {
const user = await userServiceClient.getUser({ name });
return { name, user };
} catch {
return { name, user: undefined };
}
}),
);
const userMap = new Map<string, User | undefined>();
for (const { name, user } of users) {
userMap.set(name, user);
}
return userMap;
},
enabled,
staleTime: 1000 * 60 * 5, // 5 minutes - user profiles don't change often
});
}