Repository: Couchers-org/couchers (language: python)

Issue:
Use LiteUser in lots of places in the frontend
The frontend currently loads the user's whole profile in most cases, even if all it needs is the name + avatar. We discussed this at a few dev meetings, and for now I've implemented a `GetLiteUser` RPC that returns just the minimal info needed in the majority of cases in the backend. The main places where this does NOT work are the profile itself, and the map (that's a separate exercise).

In addition to the lite user RPC, there is also the `GetLiteUsers` RPC that gets many users: this allows fetching up to 200 users at a time in one backend call, which should significantly reduce the number of calls required e.g. when opening the map page.

This issue requires:
1. Creating a parallel `useLiteUser(s)` hook similat to the existing `useUser(s)`  hook, and
2. Updating many uses of the latter hook to the new former hook.

https://github.com/Couchers-org/couchers/blob/711efe78776efad8ccb331fda32eb47d6b1af13c/app/proto/api.proto#L22-L28


Discussion / hints:

Fix patch:
diff --git a/app/web/components/Avatar/Avatar.tsx b/app/web/components/Avatar/Avatar.tsx
index bc9c0b1bca..f3caa317dc 100644
--- a/app/web/components/Avatar/Avatar.tsx
+++ b/app/web/components/Avatar/Avatar.tsx
@@ -3,7 +3,7 @@ import { makeStyles } from "@material-ui/core/styles";
 import { Skeleton } from "@material-ui/lab";
 import classNames from "classnames";
 import Link from "next/link";
-import { User } from "proto/api_pb";
+import { LiteUser } from "proto/api_pb";
 import React from "react";
 import { routeToUser } from "routes";
 
@@ -44,7 +44,7 @@ const useStyles = makeStyles((theme) => ({
 
 export interface AvatarProps {
   children?: React.ReactNode;
-  user?: User.AsObject;
+  user?: LiteUser.AsObject;
   grow?: boolean;
   className?: string;
   isProfileLink?: boolean;
diff --git a/app/web/components/UserSummary.tsx b/app/web/components/UserSummary.tsx
index 6c76ca953a..c86635fb64 100644
--- a/app/web/components/UserSummary.tsx
+++ b/app/web/components/UserSummary.tsx
@@ -4,7 +4,7 @@ import classNames from "classnames";
 import Avatar from "components/Avatar";
 import { OpenInNewIcon } from "components/Icons";
 import StyledLink from "components/StyledLink";
-import { User } from "proto/api_pb";
+import { LiteUser } from "proto/api_pb";
 import { routeToUser } from "routes";
 import makeStyles from "utils/makeStyles";
 
@@ -58,7 +58,7 @@ export interface UserSummaryProps {
   smallAvatar?: boolean;
   nameOnly?: boolean;
   headlineComponent?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
-  user?: User.AsObject;
+  user?: LiteUser.AsObject;
   titleIsLink?: boolean;
 }
 
diff --git a/app/web/features/communities/discussions/Comment.tsx b/app/web/features/communities/discussions/Comment.tsx
index daaab1ad89..28a849c9b5 100644
--- a/app/web/features/communities/discussions/Comment.tsx
+++ b/app/web/features/communities/discussions/Comment.tsx
@@ -3,7 +3,7 @@ import { Skeleton } from "@material-ui/lab";
 import Avatar from "components/Avatar";
 import Button from "components/Button";
 import Markdown from "components/Markdown";
-import { useUser } from "features/userQueries/useUsers";
+import { useLiteUser } from "features/userQueries/useLiteUsers";
 import { useTranslation } from "i18n";
 import { COMMUNITIES, GLOBAL } from "i18n/namespaces";
 import { Reply } from "proto/threads_pb";
@@ -81,7 +81,7 @@ interface CommentProps {
 export default function Comment({ topLevel = false, comment }: CommentProps) {
   const { t } = useTranslation([GLOBAL, COMMUNITIES]);
   const classes = useStyles();
-  const { data: user, isLoading: isUserLoading } = useUser(
+  const { data: user, isLoading: isUserLoading } = useLiteUser(
     comment.authorUserId
   );
 
diff --git a/app/web/features/communities/discussions/DiscussionCard.tsx b/app/web/features/communities/discussions/DiscussionCard.tsx
index fc593ed583..12e1fdbee3 100644
--- a/app/web/features/communities/discussions/DiscussionCard.tsx
+++ b/app/web/features/communities/discussions/DiscussionCard.tsx
@@ -2,7 +2,7 @@ import { Card, CardContent, Typography } from "@material-ui/core";
 import { Skeleton } from "@material-ui/lab";
 import classNames from "classnames";
 import Avatar from "components/Avatar";
-import { useUser } from "features/userQueries/useUsers";
+import { useLiteUser } from "features/userQueries/useLiteUsers";
 import { useTranslation } from "i18n";
 import { COMMUNITIES } from "i18n/namespaces";
 import Link from "next/link";
@@ -62,7 +62,7 @@ export default function DiscussionCard({
 }) {
   const { t } = useTranslation([COMMUNITIES]);
   const classes = useStyles();
-  const { data: creator } = useUser(discussion.creatorUserId);
+  const { data: creator } = useLiteUser(discussion.creatorUserId);
 
   const date = discussion.created
     ? timestamp2Date(discussion.created)
diff --git a/app/web/features/communities/discussions/DiscussionPage.tsx b/app/web/features/communities/discussions/DiscussionPage.tsx
index f0d61f33d8..1ef00d4170 100644
--- a/app/web/features/communities/discussions/DiscussionPage.tsx
+++ b/app/web/features/communities/discussions/DiscussionPage.tsx
@@ -9,7 +9,7 @@ import { BackIcon } from "components/Icons";
 import Markdown from "components/Markdown";
 import PageTitle from "components/PageTitle";
 import { discussionKey } from "features/queryKeys";
-import { useUser } from "features/userQueries/useUsers";
+import { useLiteUser } from "features/userQueries/useLiteUsers";
 import { RpcError } from "grpc-web";
 import { useTranslation } from "i18n";
 import { COMMUNITIES, GLOBAL } from "i18n/namespaces";
@@ -80,7 +80,7 @@ export default function DiscussionPage({
     queryFn: () => service.discussions.getDiscussion(discussionId),
   });
 
-  const { data: discussionCreator, isLoading: isCreatorLoading } = useUser(
+  const { data: discussionCreator, isLoading: isCreatorLoading } = useLiteUser(
     discussion?.creatorUserId
   );
 
diff --git a/app/web/features/communities/events/EventUsers.tsx b/app/web/features/communities/events/EventUsers.tsx
index d9cc71d4e7..7c5afdce62 100644
--- a/app/web/features/communities/events/EventUsers.tsx
+++ b/app/web/features/communities/events/EventUsers.tsx
@@ -2,7 +2,7 @@ import { Card, CircularProgress, Typography } from "@material-ui/core";
 import Alert from "components/Alert";
 import Button from "components/Button";
 import UserSummary from "components/UserSummary";
-import useUsers from "features/userQueries/useUsers";
+import { useLiteUsers } from "features/userQueries/useLiteUsers";
 import { RpcError } from "grpc-web";
 import { useTranslation } from "i18n";
 import { COMMUNITIES } from "i18n/namespaces";
@@ -30,7 +30,7 @@ export interface EventUsersProps {
   isUsersRefetching: boolean;
   onSeeAllClick?(): void;
   userIds: number[];
-  users: ReturnType<typeof useUsers>["data"];
+  users: ReturnType<typeof useLiteUsers>["data"];
   title: string;
 }
 
diff --git a/app/web/features/connections/friends/FriendSummaryView.tsx b/app/web/features/connections/friends/FriendSummaryView.tsx
index cc265942b8..9fa3765ac0 100644
--- a/app/web/features/connections/friends/FriendSummaryView.tsx
+++ b/app/web/features/connections/friends/FriendSummaryView.tsx
@@ -1,5 +1,5 @@
 import UserSummary from "components/UserSummary";
-import { User } from "proto/api_pb";
+import { LiteUser } from "proto/api_pb";
 import makeStyles from "utils/makeStyles";
 
 const useStyles = makeStyles((theme) => ({
@@ -16,7 +16,7 @@ const useStyles = makeStyles((theme) => ({
 
 interface FriendSummaryViewProps {
   children?: React.ReactNode;
-  friend?: User.AsObject;
+  friend?: LiteUser.AsObject;
 }
 
 export const FRIEND_ITEM_TEST_ID = "friend-item";
diff --git a/app/web/features/connections/friends/useFriendRequests.ts b/app/web/features/connections/friends/useFriendRequests.ts
index 66a3af3f3c..1a43455c96 100644
--- a/app/web/features/connections/friends/useFriendRequests.ts
+++ b/app/web/features/connections/friends/useFriendRequests.ts
@@ -1,5 +1,5 @@
 import { friendRequestKey, FriendRequestType } from "features/queryKeys";
-import useUsers from "features/userQueries/useUsers";
+import { useLiteUsers } from "features/userQueries/useLiteUsers";
 import { FriendRequest } from "proto/api_pb";
 import { useQuery } from "react-query";
 import { service } from "service";
@@ -26,22 +26,27 @@ export default function useFriendRequests(
   );
 
   const {
-    data: usersData,
-    isLoading: isUsersLoading,
-    errors: usersErrors,
-  } = useUsers(userIds);
-
-  const errors = error ? [error.message, ...usersErrors] : usersErrors;
-
-  const isLoading = isFriendReqLoading || isUsersLoading;
-
-  const data =
-    !isLoading && usersData
-      ? (friendRequestLists ?? []).map((friendRequest) => ({
-          ...friendRequest,
-          friend: usersData.get(friendRequest.userId),
-        }))
-      : void 0;
+    data: liteUsersData,
+    isLoading: isLiteUsersLoading,
+    error: liteUserError,
+  } = useLiteUsers(userIds);
+
+  const errors = error
+    ? [error.message, liteUserError?.message]
+    : liteUserError?.message
+    ? [liteUserError.message]
+    : [];
+
+  const isLoading = isFriendReqLoading || isLiteUsersLoading;
+
+  const formattedFriendRequests = !liteUsersData
+    ? []
+    : (friendRequestLists ?? []).map((friendRequest) => ({
+        ...friendRequest,
+        friend: liteUsersData.get(friendRequest.userId),
+      }));
+
+  const data = !isLoading ? formattedFriendRequests : undefined;
 
   return {
     data,
diff --git a/app/web/features/messages/groupchats/AdminsDialog.tsx b/app/web/features/messages/groupchats/AdminsDialog.tsx
index e2ebcd1bfd..60d1c78139 100644
--- a/app/web/features/messages/groupchats/AdminsDialog.tsx
+++ b/app/web/features/messages/groupchats/AdminsDialog.tsx
@@ -24,12 +24,12 @@ import {
   groupChatMessagesKey,
   groupChatsListKey,
 } from "features/queryKeys";
-import useUsers from "features/userQueries/useUsers";
+import { useLiteUsers } from "features/userQueries/useLiteUsers";
 import { Empty } from "google-protobuf/google/protobuf/empty_pb";
 import { RpcError } from "grpc-web";
 import { useTranslation } from "i18n";
 import { GLOBAL, MESSAGES } from "i18n/namespaces";
-import { User } from "proto/api_pb";
+import { LiteUser } from "proto/api_pb";
 import { GroupChat } from "proto/conversations_pb";
 import React, { useEffect, useState } from "react";
 import { useMutation, useQueryClient } from "react-query";
@@ -42,7 +42,7 @@ function AdminListItem({
   setError,
 }: {
   groupChatId: number;
-  member: User.AsObject;
+  member: LiteUser.AsObject;
   memberIsAdmin: boolean;
   setError: (value: string) => void;
 }) {
@@ -175,8 +175,8 @@ export default function AdminsDialog({
   );
 
   const currentUserId = useAuthContext().authState.userId;
-  const admins = useUsers(groupChat?.adminUserIdsList ?? []);
-  const nonAdmins = useUsers(nonAdminIds ?? []);
+  const admins = useLiteUsers(groupChat?.adminUserIdsList ?? []);
+  const nonAdmins = useLiteUsers(nonAdminIds ?? []);
   const onClose = props?.onClose;
   const isOpen = props.open;
 
diff --git a/app/web/features/messages/groupchats/GroupChatListItem.tsx b/app/web/features/messages/groupchats/GroupChatListItem.tsx
index 29ff6fa442..ee35c0a12f 100644
--- a/app/web/features/messages/groupchats/GroupChatListItem.tsx
+++ b/app/web/features/messages/groupchats/GroupChatListItem.tsx
@@ -15,7 +15,7 @@ import {
   isControlMessage,
   messageTargetId,
 } from "features/messages/utils";
-import useUsers from "features/userQueries/useUsers";
+import { useLiteUsers } from "features/userQueries/useLiteUsers";
 import { useTranslation } from "i18n";
 import { MESSAGES } from "i18n/namespaces";
 import { GroupChat } from "proto/conversations_pb";
@@ -47,7 +47,7 @@ export default function GroupChatListItem({
 
   //It is possible the last message is sent by someone who has left
   //so include it just in case
-  const groupChatMembersQuery = useUsers([
+  const groupChatMembersQuery = useLiteUsers([
     ...groupChat.memberUserIdsList,
     latestMessageAuthorId,
   ]);
diff --git a/app/web/features/messages/groupchats/GroupChatView.tsx b/app/web/features/messages/groupchats/GroupChatView.tsx
index f809a628d7..35807a4109 100644
--- a/app/web/features/messages/groupchats/GroupChatView.tsx
+++ b/app/web/features/messages/groupchats/GroupChatView.tsx
@@ -26,7 +26,7 @@ import {
   groupChatMessagesKey,
   groupChatsListKey,
 } from "features/queryKeys";
-import useUsers from "features/userQueries/useUsers";
+import { useLiteUsers } from "features/userQueries/useLiteUsers";
 import { Empty } from "google-protobuf/google/protobuf/empty_pb";
 import { RpcError } from "grpc-web";
 import { useTranslation } from "i18n";
@@ -160,7 +160,9 @@ export default function GroupChatView({ chatId }: { chatId: number }) {
   //for title text
   const currentUserId = useAuthContext().authState.userId!;
   const isChatAdmin = groupChat?.adminUserIdsList.includes(currentUserId);
-  const groupChatMembersQuery = useUsers(groupChat?.memberUserIdsList ?? []);
+  const groupChatMembersQuery = useLiteUsers(
+    groupChat?.memberUserIdsList ?? []
+  );
 
   const {
     data: messagesRes,
diff --git a/app/web/features/messages/groupchats/MembersDialog.tsx b/app/web/features/messages/groupchats/MembersDialog.tsx
index 1c9a551f16..1703497c6d 100644
--- a/app/web/features/messages/groupchats/MembersDialog.tsx
+++ b/app/web/features/messages/groupchats/MembersDialog.tsx
@@ -13,10 +13,10 @@ import {
   DialogTitle,
 } from "components/Dialog";
 import TextBody from "components/TextBody";
-import useUsers from "features/userQueries/useUsers";
+import { useLiteUsers } from "features/userQueries/useLiteUsers";
 import { useTranslation } from "i18n";
 import { GLOBAL, MESSAGES } from "i18n/namespaces";
-import { User } from "proto/api_pb";
+import { LiteUser } from "proto/api_pb";
 import { GroupChat } from "proto/conversations_pb";
 import React from "react";
 import makeStyles from "utils/makeStyles";
@@ -38,7 +38,7 @@ function MemberListItem({
   member,
   memberIsAdmin,
 }: {
-  member: User.AsObject;
+  member: LiteUser.AsObject;
   memberIsAdmin: boolean;
 }) {
   const classes = useMembersDialogStyles();
@@ -59,7 +59,7 @@ export default function MembersDialog({
   ...props
 }: DialogProps & { groupChat?: GroupChat.AsObject }) {
   const { t } = useTranslation([GLOBAL, MESSAGES]);
-  const members = useUsers(groupChat?.memberUserIdsList ?? []);
+  const members = useLiteUsers(groupChat?.memberUserIdsList ?? []);
 
   return (
     <Dialog {...props} aria-labelledby="members-dialog-title">
diff --git a/app/web/features/messages/messagelist/ControlMessageView.tsx b/app/web/features/messages/messagelist/ControlMessageView.tsx
index 17c42c6744..7dc500a299 100644
--- a/app/web/features/messages/messagelist/ControlMessageView.tsx
+++ b/app/web/features/messages/messagelist/ControlMessageView.tsx
@@ -2,6 +2,7 @@ import { makeStyles } from "@material-ui/core/styles";
 import { Skeleton } from "@material-ui/lab";
 import classNames from "classnames";
 import TextBody from "components/TextBody";
+import { useLiteUser } from "features/userQueries/useLiteUsers";
 import { useTranslation } from "i18n";
 import { MESSAGES } from "i18n/namespaces";
 import React from "react";
@@ -9,7 +10,6 @@ import React from "react";
 import { timestamp2Date } from "../../../utils/date";
 import { firstName } from "../../../utils/names";
 import useOnVisibleEffect from "../../../utils/useOnVisibleEffect";
-import { useUser } from "../../userQueries/useUsers";
 import { controlMessage, messageTargetId } from "../utils";
 import { messageElementId, MessageProps } from "./MessageView";
 import TimeInterval from "./TimeInterval";
@@ -34,10 +34,10 @@ export default function ControlMessageView({
 }: MessageProps) {
   const { t } = useTranslation(MESSAGES);
   const classes = useStyles();
-  const { data: author, isLoading: isAuthorLoading } = useUser(
+  const { data: author, isLoading: isAuthorLoading } = useLiteUser(
     message.authorUserId
   );
-  const { data: target, isLoading: isTargetLoading } = useUser(
+  const { data: target, isLoading: isTargetLoading } = useLiteUser(
     messageTargetId(message)
   );
   const { ref } = useOnVisibleEffect(onVisible);
diff --git a/app/web/features/messages/messagelist/MessageView.tsx b/app/web/features/messages/messagelist/MessageView.tsx
index 9a2d0de849..733921e7e2 100644
--- a/app/web/features/messages/messagelist/MessageView.tsx
+++ b/app/web/features/messages/messagelist/MessageView.tsx
@@ -8,7 +8,7 @@ import TextBody from "components/TextBody";
 import FlagButton from "features/FlagButton";
 import TimeInterval from "features/messages/messagelist/TimeInterval";
 import useCurrentUser from "features/userQueries/useCurrentUser";
-import { useUser } from "features/userQueries/useUsers";
+import { useLiteUser } from "features/userQueries/useLiteUsers";
 import { Message } from "proto/conversations_pb";
 import { timestamp2Date } from "utils/date";
 import useOnVisibleEffect from "utils/useOnVisibleEffect";
@@ -91,7 +91,7 @@ export default function MessageView({
   className,
 }: MessageProps) {
   const classes = useStyles();
-  const { data: author, isLoading: isAuthorLoading } = useUser(
+  const { data: author, isLoading: isAuthorLoading } = useLiteUser(
     message.authorUserId
   );
   const { data: currentUser, isLoading: isCurrentUserLoading } =
diff --git a/app/web/features/messages/requests/HostRequestListItem.tsx b/app/web/features/messages/requests/HostRequestListItem.tsx
index 66a9788fdb..b6c97c2040 100644
--- a/app/web/features/messages/requests/HostRequestListItem.tsx
+++ b/app/web/features/messages/requests/HostRequestListItem.tsx
@@ -18,7 +18,7 @@ import {
   messageTargetId,
 } from "features/messages/utils";
 import useCurrentUser from "features/userQueries/useCurrentUser";
-import { useUser } from "features/userQueries/useUsers";
+import { useLiteUser } from "features/userQueries/useLiteUsers";
 import { useTranslation } from "i18n";
 import { MESSAGES } from "i18n/namespaces";
 import { HostRequest } from "proto/requests_pb";
@@ -52,7 +52,7 @@ export default function HostRequestListItem({
   const { authState } = useAuthContext();
   const isHost = authState.userId === hostRequest.hostUserId;
   const { data: currentUser } = useCurrentUser();
-  const { data: otherUser, isLoading: isOtherUserLoading } = useUser(
+  const { data: otherUser, isLoading: isOtherUserLoading } = useLiteUser(
     isHost ? hostRequest.surferUserId : hostRequest.hostUserId
   );
   const isUnread =
diff --git a/app/web/features/messages/requests/HostRequestView.tsx b/app/web/features/messages/requests/HostRequestView.tsx
index d071d6fec1..926200e69b 100644
--- a/app/web/features/messages/requests/HostRequestView.tsx
+++ b/app/web/features/messages/requests/HostRequestView.tsx
@@ -20,7 +20,7 @@ import {
   hostRequestMessagesKey,
   hostRequestsListKey,
 } from "features/queryKeys";
-import { useUser } from "features/userQueries/useUsers";
+import { useLiteUser } from "features/userQueries/useLiteUsers";
 import { Empty } from "google-protobuf/google/protobuf/empty_pb";
 import { RpcError } from "grpc-web";
 import { useTranslation } from "i18n";
@@ -81,8 +81,8 @@ export default function HostRequestView({
     }
   );
 
-  const { data: surfer } = useUser(hostRequest?.surferUserId);
-  const { data: host } = useUser(hostRequest?.hostUserId);
+  const { data: surfer } = useLiteUser(hostRequest?.surferUserId);
+  const { data: host } = useLiteUser(hostRequest?.hostUserId);
   const currentUserId = useAuthContext().authState.userId;
   const isHost = host?.userId === currentUserId;
   const otherUser = isHost ? surfer : host;
diff --git a/app/web/features/messages/utils.ts b/app/web/features/messages/utils.ts
index cd67919fde..33ee7e9d25 100644
--- a/app/web/features/messages/utils.ts
+++ b/app/web/features/messages/utils.ts
@@ -1,4 +1,4 @@
-import useUsers from "features/userQueries/useUsers";
+import { useLiteUsers } from "features/userQueries/useLiteUsers";
 import { GroupChat, Message } from "proto/conversations_pb";
 import { TFunction } from "react-i18next";
 import { firstName } from "utils/names";
@@ -66,7 +66,7 @@ export function controlMessage({
 
 export function groupChatTitleText(
   groupChat: GroupChat.AsObject,
-  groupChatMembersQuery: ReturnType<typeof useUsers>,
+  groupChatMembersQuery: ReturnType<typeof useLiteUsers>,
   currentUserId: number
 ) {
   return groupChat.title
@@ -81,7 +81,7 @@ export function groupChatTitleText(
 
 /** Returns the other user's username, or null if there are more than 2 users. */
 export function getDmUsername(
-  groupChatMembersQuery: ReturnType<typeof useUsers>,
+  groupChatMembersQuery: ReturnType<typeof useLiteUsers>,
   currentUserId: number
 ) {
   const users = Array.from(groupChatMembersQuery.data?.values() ?? []);
diff --git a/app/web/features/profile/view/NewHostRequest.tsx b/app/web/features/profile/view/NewHostRequest.tsx
index 8e16fdfebd..5cf303b4a4 100644
--- a/app/web/features/profile/view/NewHostRequest.tsx
+++ b/app/web/features/profile/view/NewHostRequest.tsx
@@ -14,7 +14,7 @@ import Button from "components/Button";
 import Datepicker from "components/Datepicker";
 import TextField from "components/TextField";
 import { useProfileUser } from "features/profile/hooks/useProfileUser";
-import { useUser } from "features/userQueries/useUsers";
+import { useLiteUser } from "features/userQueries/useLiteUsers";
 import { RpcError } from "grpc-web";
 import { useTranslation } from "i18n";
 import { GLOBAL, PROFILE } from "i18n/namespaces";
@@ -102,7 +102,7 @@ export default function NewHostRequest({
     }
   );
 
-  const { isLoading: hostLoading, error: hostError } = useUser(user.userId);
+  const { isLoading: hostLoading, error: hostError } = useLiteUser(user.userId);
 
   const onSubmit = handleSubmit((data) => {
     mutate(data);
@@ -139,7 +139,7 @@ export default function NewHostRequest({
       </Typography>
       {error && <Alert severity="error">{error.message}</Alert>}
       {hostError ? (
-        <Alert severity={"error"}>{hostError}</Alert>
+        <Alert severity={"error"}>{hostError?.message}</Alert>
       ) : (
         <form onSubmit={onSubmit}>
           <div className={classes.request}>
diff --git a/app/web/features/profile/view/ReferenceList.tsx b/app/web/features/profile/view/ReferenceList.tsx
index 9ab89c1664..ccfb512fb8 100644
--- a/app/web/features/profile/view/ReferenceList.tsx
+++ b/app/web/features/profile/view/ReferenceList.tsx
@@ -1,5 +1,5 @@
 import { List } from "@material-ui/core";
-import useUsers from "features/userQueries/useUsers";
+import { useLiteUsers } from "features/userQueries/useLiteUsers";
 import { ListReferencesRes } from "proto/references_pb";
 import makeStyles from "utils/makeStyles";
 
@@ -17,7 +17,7 @@ const useStyles = makeStyles((theme) => ({
 interface ReferenceListProps {
   isReceived?: boolean;
   referencePages: ListReferencesRes.AsObject[];
-  referenceUsers: ReturnType<typeof useUsers>["data"];
+  referenceUsers: ReturnType<typeof useLiteUsers>["data"];
 }
 
 export default function ReferenceList({
diff --git a/app/web/features/profile/view/ReferenceListItem.tsx b/app/web/features/profile/view/ReferenceListItem.tsx
index 4b04cf3d00..f29ce2550c 100644
--- a/app/web/features/profile/view/ReferenceListItem.tsx
+++ b/app/web/features/profile/view/ReferenceListItem.tsx
@@ -5,7 +5,7 @@ import UserSummary from "components/UserSummary";
 import { referenceBadgeLabel } from "features/profile/constants";
 import { COMMUNITIES, GLOBAL } from "i18n/namespaces";
 import { useTranslation } from "next-i18next";
-import { User } from "proto/api_pb";
+import { LiteUser } from "proto/api_pb";
 import { Reference } from "proto/references_pb";
 import { dateTimeFormatter, timestamp2Date } from "utils/date";
 import makeStyles from "utils/makeStyles";
@@ -43,7 +43,7 @@ export const REFERENCE_LIST_ITEM_TEST_ID = "reference-list-item";
 
 interface ReferenceListItemProps {
   isReceived: boolean;
-  user: User.AsObject;
+  user: LiteUser.AsObject;
   reference: Reference.AsObject;
 }
 
diff --git a/app/web/features/profile/view/ReferencesGivenList.tsx b/app/web/features/profile/view/ReferencesGivenList.tsx
index 81e5e1ccfc..79d278a90d 100644
--- a/app/web/features/profile/view/ReferencesGivenList.tsx
+++ b/app/web/features/profile/view/ReferencesGivenList.tsx
@@ -1,4 +1,4 @@
-import useUsers from "features/userQueries/useUsers";
+import { useLiteUsers } from "features/userQueries/useLiteUsers";
 
 import { useReferencesGiven } from "../hooks/referencesHooks";
 import { useProfileUser } from "../hooks/useProfileUser";
@@ -13,7 +13,7 @@ export default function ReferencesGivenList() {
       .map((page) => page.referencesList.map((reference) => reference.toUserId))
       .flat() ?? [];
   const { data: referenceUsers, isLoading: isReferenceUsersLoading } =
-    useUsers(userIds);
+    useLiteUsers(userIds);
 
   return (
     <ReferencesView
diff --git a/app/web/features/profile/view/ReferencesReceivedList.tsx b/app/web/features/profile/view/ReferencesReceivedList.tsx
index 07be9fc8e3..5c00fd963a 100644
--- a/app/web/features/profile/view/ReferencesReceivedList.tsx
+++ b/app/web/features/profile/view/ReferencesReceivedList.tsx
@@ -1,4 +1,4 @@
-import useUsers from "features/userQueries/useUsers";
+import { useLiteUsers } from "features/userQueries/useLiteUsers";
 
 import { useReferencesReceived } from "../hooks/referencesHooks";
 import { useProfileUser } from "../hooks/useProfileUser";
@@ -22,7 +22,7 @@ export default function ReferencesReceivedList({
       )
       .flat() ?? [];
   const { data: referenceUsers, isLoading: isReferenceUsersLoading } =
-    useUsers(userIds);
+    useLiteUsers(userIds);
 
   return (
     <ReferencesView
diff --git a/app/web/features/profile/view/ReferencesView.tsx b/app/web/features/profile/view/ReferencesView.tsx
index 342a3ff259..255ddef291 100644
--- a/app/web/features/profile/view/ReferencesView.tsx
+++ b/app/web/features/profile/view/ReferencesView.tsx
@@ -2,7 +2,7 @@ import Alert from "components/Alert";
 import Button from "components/Button";
 import CircularProgress from "components/CircularProgress";
 import TextBody from "components/TextBody";
-import useUsers from "features/userQueries/useUsers";
+import { useLiteUsers } from "features/userQueries/useLiteUsers";
 import { RpcError } from "grpc-web";
 import { useTranslation } from "i18n";
 import { GLOBAL, PROFILE } from "i18n/namespaces";
@@ -17,7 +17,7 @@ interface ReferencesViewProps {
   isReceived?: boolean;
   isReferenceUsersLoading: boolean;
   referencesQuery: UseInfiniteQueryResult<ListReferencesRes.AsObject, RpcError>;
-  referenceUsers: ReturnType<typeof useUsers>["data"];
+  referenceUsers: ReturnType<typeof useLiteUsers>["data"];
 }
 
 export const useReferencesViewStyles = makeStyles((theme) => ({
diff --git a/app/web/features/profile/view/leaveReference/formSteps/submit/SubmitReference.tsx b/app/web/features/profile/view/leaveReference/formSteps/submit/SubmitReference.tsx
index 4dc14fc52a..e2705b5e35 100644
--- a/app/web/features/profile/view/leaveReference/formSteps/submit/SubmitReference.tsx
+++ b/app/web/features/profile/view/leaveReference/formSteps/submit/SubmitReference.tsx
@@ -18,7 +18,7 @@ import {
   ReferenceContextFormData,
   useReferenceStyles,
 } from "features/profile/view/leaveReference/ReferenceForm";
-import { useUser } from "features/userQueries/useUsers";
+import { useLiteUser } from "features/userQueries/useLiteUsers";
 import { useTranslation } from "i18n";
 import { GLOBAL, PROFILE } from "i18n/namespaces";
 import { useRouter } from "next/router";
@@ -73,7 +73,7 @@ export default function SubmitReference({
   const isSmOrWider = useMediaQuery(theme.breakpoints.up("sm"));
   const { handleSubmit } = useForm<ReferenceContextFormData>();
 
-  const userQuery = useUser(userId);
+  const userQuery = useLiteUser(userId);
   const onFriendReferenceSubmit = () => {
     const formData: WriteFriendReferenceInput =
       referenceData.wasAppropriate === "true"
diff --git a/app/web/features/queryKeys.ts b/app/web/features/queryKeys.ts
index b0427f34e3..043f4e7acd 100644
--- a/app/web/features/queryKeys.ts
+++ b/app/web/features/queryKeys.ts
@@ -15,6 +15,12 @@ export function userKey(userId?: number) {
   return userId === undefined ? "user" : ["user", userId];
 }
 
+export function liteUserKey(userId?: number) {
+  return userId === undefined ? "liteUser" : ["liteUser", userId];
+}
+
+export const liteUsersKey = (ids: number[]) => ["liteUsers", ...ids];
+
 export function responseRateKey(userId: number) {
   return ["responseRate", userId];
 }
diff --git a/app/web/features/userQueries/useLiteUsers.ts b/app/web/features/userQueries/useLiteUsers.ts
new file mode 100644
index 0000000000..f16a7e65f5
--- /dev/null
+++ b/app/web/features/userQueries/useLiteUsers.ts
@@ -0,0 +1,64 @@
+import { reactQueryRetries } from "appConstants";
+import { liteUserKey, liteUsersKey } from "features/queryKeys";
+import { RpcError, StatusCode } from "grpc-web";
+import { GetLiteUsersRes, LiteUser } from "proto/api_pb";
+import { useQuery } from "react-query";
+import { service } from "service";
+
+import { userStaleTime } from "./constants";
+
+// React Query typically retains the last successful data until the next successful fetch
+function useLiteUsers(ids: (number | undefined)[]) {
+  const nonFalseyIds = ids?.filter((id): id is number => !!id);
+  const query = useQuery<GetLiteUsersRes.AsObject, RpcError>({
+    queryKey: liteUsersKey(nonFalseyIds),
+    queryFn: () => {
+      const result = service.user.getLiteUsers(nonFalseyIds);
+      return result;
+    },
+    staleTime: userStaleTime,
+    enabled: nonFalseyIds.length > 0, // run only if there are valid liteUserIds
+    retry: (failureCount, error) => {
+      // don't retry if the user isn't found
+      return (
+        error.code !== StatusCode.NOT_FOUND && failureCount < reactQueryRetries
+      );
+    },
+  });
+
+  const isDataUndefined = !query.data || !query.data.responsesList;
+  const usersById =
+    query.isLoading || isDataUndefined
+      ? undefined
+      : new Map(
+          query?.data?.responsesList.map((response) => [
+            response?.user?.userId,
+            response.user,
+          ])
+        );
+
+  return {
+    ...query,
+    data: usersById,
+  };
+}
+
+// React Query typically retains the last successful data until the next successful fetch
+function useLiteUser(id: number | undefined) {
+  const query = useQuery<LiteUser.AsObject, RpcError>({
+    queryKey: liteUserKey(id),
+    queryFn: () => service.user.getLiteUser(id?.toString() || ""),
+    staleTime: userStaleTime,
+    enabled: id !== undefined,
+    retry: (failureCount, error) => {
+      // don't retry if the user isn't found
+      return (
+        error.code !== StatusCode.NOT_FOUND && failureCount < reactQueryRetries
+      );
+    },
+  });
+
+  return query;
+}
+
+export { useLiteUser, useLiteUsers };
diff --git a/app/web/service/conversations.ts b/app/web/service/conversations.ts
index 472b878747..85a18a3672 100644
--- a/app/web/service/conversations.ts
+++ b/app/web/service/conversations.ts
@@ -3,7 +3,7 @@ import {
   StringValue,
 } from "google-protobuf/google/protobuf/wrappers_pb";
 import { StatusCode } from "grpc-web";
-import { User } from "proto/api_pb";
+import { LiteUser, User } from "proto/api_pb";
 import {
   CreateGroupChatReq,
   EditGroupChatReq,
@@ -92,14 +92,20 @@ export function inviteToGroupChat(groupChatId: number, users: User.AsObject[]) {
   return Promise.all(promises);
 }
 
-export function makeGroupChatAdmin(groupChatId: number, user: User.AsObject) {
+export function makeGroupChatAdmin(
+  groupChatId: number,
+  user: LiteUser.AsObject
+) {
   const req = new MakeGroupChatAdminReq();
   req.setGroupChatId(groupChatId);
   req.setUserId(user.userId);
   return client.conversations.makeGroupChatAdmin(req);
 }
 
-export function removeGroupChatAdmin(groupChatId: number, user: User.AsObject) {
+export function removeGroupChatAdmin(
+  groupChatId: number,
+  user: LiteUser.AsObject
+) {
   const req = new RemoveGroupChatAdminReq();
   req.setGroupChatId(groupChatId);
   req.setUserId(user.userId);
diff --git a/app/web/service/user.ts b/app/web/service/user.ts
index 13492ea521..ba6835ba49 100644
--- a/app/web/service/user.ts
+++ b/app/web/service/user.ts
@@ -1,6 +1,8 @@
 import { Empty } from "google-protobuf/google/protobuf/empty_pb";
 import wrappers from "google-protobuf/google/protobuf/wrappers_pb";
 import {
+  GetLiteUserReq,
+  GetLiteUsersReq,
   GetUserReq,
   LanguageAbility,
   NullableBoolValue,
@@ -101,6 +103,32 @@ export async function getUser(user: string): Promise<User.AsObject> {
   return response.toObject();
 }
 
+/**
+ * Returns LiteUser record by Username or id
+ *
+ * @param {string} user
+ * @returns {Promise<LiteUser.AsObject>}
+ */
+export async function getLiteUser(user: string) {
+  const userReq = new GetLiteUserReq();
+  userReq.setUser(user || "");
+
+  const response = await client.api.getLiteUser(userReq);
+
+  return response.toObject();
+}
+
+/** Returns LiteUsers by ids
+ */
+export async function getLiteUsers(userIds: number[]) {
+  const req = new GetLiteUsersReq();
+  req.setUsersList(userIds.map(String));
+
+  const response = await client.api.getLiteUsers(req);
+
+  return response.toObject();
+}
+
 /**
  * Updates user profile
  */


Test patch:
diff --git a/app/web/features/communities/discussions/DiscussionPage.test.tsx b/app/web/features/communities/discussions/DiscussionPage.test.tsx
index 37d16276e0..4cb0ef14b9 100644
--- a/app/web/features/communities/discussions/DiscussionPage.test.tsx
+++ b/app/web/features/communities/discussions/DiscussionPage.test.tsx
@@ -14,7 +14,7 @@ import comments from "test/fixtures/comments.json";
 import community from "test/fixtures/community.json";
 import discussions from "test/fixtures/discussions.json";
 import { getHookWrapperWithClient } from "test/hookWrapper";
-import { getThread, getUser } from "test/serviceMockDefaults";
+import { getLiteUser, getThread } from "test/serviceMockDefaults";
 import {
   assertErrorAlert,
   mockConsoleError,
@@ -28,8 +28,8 @@ import DiscussionPage, { CREATOR_TEST_ID } from "./DiscussionPage";
 
 jest.mock("components/MarkdownInput");
 
-const getUserMock = service.user.getUser as MockedService<
-  typeof service.user.getUser
+const getLiteUserMock = service.user.getLiteUser as MockedService<
+  typeof service.user.getLiteUser
 >;
 const getCommunityMock = service.communities.getCommunity as MockedService<
   typeof service.communities.getCommunity
@@ -94,7 +94,7 @@ describe("Discussion page", () => {
   });
 
   beforeEach(() => {
-    getUserMock.mockImplementation(getUser);
+    getLiteUserMock.mockImplementation(getLiteUser);
     getCommunityMock.mockResolvedValue(community);
     getDiscussionMock.mockResolvedValue(discussions[0]);
     getThreadMock.mockImplementation(getThread);
@@ -130,7 +130,9 @@ describe("Discussion page", () => {
   });
 
   it("renders a loading skeleton if the user info is still loading", async () => {
-    getUserMock.mockImplementation(async () => new Promise(() => undefined));
+    getLiteUserMock.mockImplementation(
+      async () => new Promise(() => undefined)
+    );
     renderDiscussion();
     await waitForElementToBeRemoved(screen.getByRole("progressbar"));
 
@@ -168,7 +170,7 @@ describe("Discussion page", () => {
     ).toBeVisible();
     // check top level comment
     const firstTopLevelComment = comments.find((c) => c.threadId === 6);
-    const commentUser = await getUser(
+    const commentUser = await getLiteUser(
       firstTopLevelComment!.authorUserId.toString()
     );
     expect(
@@ -194,7 +196,7 @@ describe("Discussion page", () => {
     ).toBeVisible();
 
     // check nested comment/reply
-    const replyUser = await getUser("3");
+    const replyUser = await getLiteUser("3");
     expect(
       commentCards[1].getByRole("img", { name: replyUser.name })
     ).toBeVisible();
diff --git a/app/web/features/communities/discussions/DiscussionsListPage.test.tsx b/app/web/features/communities/discussions/DiscussionsListPage.test.tsx
index 24f75f8eac..e5304fc93c 100644
--- a/app/web/features/communities/discussions/DiscussionsListPage.test.tsx
+++ b/app/web/features/communities/discussions/DiscussionsListPage.test.tsx
@@ -9,7 +9,7 @@ import { service } from "service";
 import community from "test/fixtures/community.json";
 import discussions from "test/fixtures/discussions.json";
 import wrapper from "test/hookWrapper";
-import { getUser } from "test/serviceMockDefaults";
+import { getLiteUser } from "test/serviceMockDefaults";
 import {
   assertErrorAlert,
   mockConsoleError,
@@ -22,8 +22,8 @@ import DiscussionsListPage from "./DiscussionsListPage";
 
 jest.mock("components/MarkdownInput");
 
-const getUserMock = service.user.getUser as MockedService<
-  typeof service.user.getUser
+const getLiteUserMock = service.user.getLiteUser as MockedService<
+  typeof service.user.getLiteUser
 >;
 const createDiscussionMock = service.discussions
   .createDiscussion as MockedService<
@@ -34,7 +34,7 @@ const listDiscussionsMock = service.communities
 
 describe("DiscussionsListPage", () => {
   beforeEach(() => {
-    getUserMock.mockImplementation(getUser);
+    getLiteUserMock.mockImplementation(getLiteUser);
     listDiscussionsMock.mockResolvedValue({
       discussionsList: discussions,
       nextPageToken: "",
@@ -50,7 +50,9 @@ describe("DiscussionsListPage", () => {
       await screen.findAllByTestId(DISCUSSION_CARD_TEST_ID)
     ).map((element) => within(element));
 
-    const firstCreator = await getUser(discussions[0].creatorUserId.toString());
+    const firstCreator = await getLiteUser(
+      discussions[0].creatorUserId.toString()
+    );
     expect(
       discussionCards[0].getByText(
         new RegExp(
@@ -72,7 +74,7 @@ describe("DiscussionsListPage", () => {
       )
     ).toBeVisible();
 
-    const secondCreator = await getUser(
+    const secondCreator = await getLiteUser(
       discussions[1].creatorUserId.toString()
     );
     expect(
diff --git a/app/web/features/connections/friends/useFriendRequests.test.tsx b/app/web/features/connections/friends/useFriendRequests.test.tsx
index e6a17cd120..2748714946 100644
--- a/app/web/features/connections/friends/useFriendRequests.test.tsx
+++ b/app/web/features/connections/friends/useFriendRequests.test.tsx
@@ -1,18 +1,19 @@
 import { renderHook } from "@testing-library/react-hooks";
 import { service } from "service";
-import users from "test/fixtures/users.json";
+import liteUsers from "test/fixtures/liteUsers.json";
 import wrapper from "test/hookWrapper";
-import { getUser } from "test/serviceMockDefaults";
+import { getLiteUsers } from "test/serviceMockDefaults";
+import { mockConsoleError } from "test/utils";
 
 import useFriendRequests from "./useFriendRequests";
 
-const getUserMock = service.user.getUser as jest.Mock;
+const getLiteUsersMock = service.user.getLiteUsers as jest.Mock;
 const listFriendRequestsMock = service.api.listFriendRequests as jest.Mock<
   ReturnType<typeof service.api.listFriendRequests>
 >;
 
 beforeEach(() => {
-  getUserMock.mockImplementation(getUser);
+  getLiteUsersMock.mockImplementation(getLiteUsers);
   listFriendRequestsMock.mockResolvedValue({
     receivedList: [],
     sentList: [],
@@ -22,7 +23,7 @@ beforeEach(() => {
 afterEach(() => jest.restoreAllMocks());
 
 describe("when the listFriendRequests query is loading", () => {
-  it("returns isLoading as true with no errors and shouldn't try to load users", async () => {
+  it("returns isLoading as true with no errors and shouldn't try to load liteUsers", async () => {
     const { result, unmount } = renderHook(() => useFriendRequests("sent"), {
       wrapper,
     });
@@ -33,7 +34,7 @@ describe("when the listFriendRequests query is loading", () => {
       isError: false,
       isLoading: true,
     });
-    expect(getUserMock).not.toHaveBeenCalled();
+    expect(getLiteUsersMock).not.toHaveBeenCalled();
 
     unmount();
   });
@@ -67,21 +68,12 @@ describe("when the listFriendRequests succeeds", () => {
     });
   });
 
-  it("returns isLoading as true with no errors if getUsers queries are loading", async () => {
-    getUserMock.mockImplementation(() => new Promise(() => void 0));
+  it("returns isLoading as true with no errors if getLiteUsers queries are loading", async () => {
+    getLiteUsersMock.mockImplementation(() => new Promise(() => void 0));
 
-    const { result, waitForNextUpdate } = renderHook(
-      () => useFriendRequests("sent"),
-      { wrapper }
-    );
-    await waitForNextUpdate();
+    const { result } = renderHook(() => useFriendRequests("sent"), { wrapper });
 
-    expect(result.current).toEqual({
-      data: undefined,
-      errors: [],
-      isError: false,
-      isLoading: true,
-    });
+    expect(result.current.isLoading).toBe(true);
   });
 
   it("returns the friend requests sent if 'Sent' is passed to the hook", async () => {
@@ -94,7 +86,7 @@ describe("when the listFriendRequests succeeds", () => {
     expect(result.current).toEqual({
       data: [
         {
-          friend: users[1],
+          friend: liteUsers[1],
           friendRequestId: 1,
           state: 0,
           userId: 2,
@@ -117,14 +109,14 @@ describe("when the listFriendRequests succeeds", () => {
     expect(result.current).toEqual({
       data: [
         {
-          friend: users[2],
+          friend: liteUsers[2],
           friendRequestId: 2,
           state: 0,
           userId: 3,
           sent: false,
         },
         {
-          friend: users[3],
+          friend: liteUsers[3],
           friendRequestId: 3,
           state: 0,
           userId: 4,
@@ -172,38 +164,20 @@ describe("when the listFriendRequests succeeds", () => {
     });
   });
 
-  it("returns isError as true with the error and missing friend data in request object of response if some getUser queries failed", async () => {
-    getUserMock.mockImplementation((userId: string) => {
-      return userId === "3"
-        ? Promise.reject(new Error(`Error fetching user ${userId}`))
-        : getUser(userId);
-    });
-    jest.spyOn(console, "error").mockReturnValue(undefined);
+  it("returns isError as true with the error if the getLiteUsers query failed", async () => {
+    const error = new Error("Error fetching users");
+    getLiteUsersMock.mockRejectedValue(error);
 
     const { result, waitForNextUpdate } = renderHook(
       () => useFriendRequests("received"),
       { wrapper }
     );
-    await waitForNextUpdate();
+    await waitForNextUpdate(); // first attempt
+    await waitForNextUpdate(); // one allowed retry
 
     expect(result.current).toEqual({
-      data: [
-        {
-          friend: undefined,
-          friendRequestId: 2,
-          state: 0,
-          userId: 3,
-          sent: false,
-        },
-        {
-          friend: users[3],
-          friendRequestId: 3,
-          state: 0,
-          userId: 4,
-          sent: false,
-        },
-      ],
-      errors: ["Error fetching user 3"],
+      data: [],
+      errors: ["Error fetching users"],
       isError: true,
       isLoading: false,
     });
@@ -211,12 +185,11 @@ describe("when the listFriendRequests succeeds", () => {
 });
 
 describe("when the listFriendRequests query failed", () => {
-  it("returns isError as true with the errors and shouldn't try to load users", async () => {
+  it("returns isError as true with the errors and shouldn't try to load liteUsers", async () => {
     listFriendRequestsMock.mockRejectedValue(
       new Error("Error listing friend requests")
     );
-    jest.spyOn(console, "error").mockReturnValue(undefined);
-
+    mockConsoleError();
     const { result, waitForNextUpdate } = renderHook(
       () => useFriendRequests("sent"),
       { wrapper }
@@ -229,6 +202,6 @@ describe("when the listFriendRequests query failed", () => {
       isError: true,
       isLoading: false,
     });
-    expect(getUserMock).not.toHaveBeenCalled();
+    expect(getLiteUsersMock).not.toHaveBeenCalled();
   });
 });
diff --git a/app/web/features/messages/groupchats/GroupChatView.test.tsx b/app/web/features/messages/groupchats/GroupChatView.test.tsx
index 6497197986..d38183bcdc 100644
--- a/app/web/features/messages/groupchats/GroupChatView.test.tsx
+++ b/app/web/features/messages/groupchats/GroupChatView.test.tsx
@@ -18,7 +18,7 @@ import {
 import { service } from "service";
 import messageData from "test/fixtures/messages.json";
 import { getHookWrapperWithClient } from "test/hookWrapper";
-import { getGroupChatMessages, getUser } from "test/serviceMockDefaults";
+import { getGroupChatMessages, getLiteUser } from "test/serviceMockDefaults";
 import {
   addDefaultUser,
   assertErrorAlert,
@@ -33,7 +33,7 @@ import { GROUP_CHAT_REFETCH_INTERVAL } from "./constants";
 const getGroupChatMock = service.conversations.getGroupChat as MockedService<
   typeof service.conversations.getGroupChat
 >;
-const getUserMock = service.user.getUser as jest.Mock;
+const getLiteUserMock = service.user.getLiteUser as jest.Mock;
 const listFriendsMock = service.api.listFriends as MockedService<
   typeof service.api.listFriends
 >;
@@ -100,7 +100,7 @@ describe("GroupChatView", () => {
   beforeEach(() => {
     getGroupChatMock.mockResolvedValue(baseGroupChatMockResponse);
     getGroupChatMessagesMock.mockImplementation(getGroupChatMessages);
-    getUserMock.mockImplementation(getUser);
+    getLiteUserMock.mockImplementation(getLiteUser);
     markLastSeenGroupChatMock.mockResolvedValue(new Empty());
     listFriendsMock.mockResolvedValue([1, 2]);
   });
@@ -123,7 +123,7 @@ describe("GroupChatView", () => {
 
       if (message.text?.text) {
         // non-control text message assertions
-        const user = await getUser(message.authorUserId.toString());
+        const user = await getLiteUser(message.authorUserId.toString());
 
         expect(
           messageElement.getByRole("heading", { name: user?.name })
diff --git a/app/web/features/profile/view/References.test.tsx b/app/web/features/profile/view/References.test.tsx
index 81a936cd02..1ff5a94a20 100644
--- a/app/web/features/profile/view/References.test.tsx
+++ b/app/web/features/profile/view/References.test.tsx
@@ -10,7 +10,7 @@ import { service } from "service";
 import references from "test/fixtures/references.json";
 import users from "test/fixtures/users.json";
 import wrapper from "test/hookWrapper";
-import { getUser } from "test/serviceMockDefaults";
+import { getLiteUser, getLiteUsers } from "test/serviceMockDefaults";
 import { MockedService, t } from "test/utils";
 
 import { referenceBadgeLabel } from "../constants";
@@ -18,8 +18,8 @@ import { ProfileUserProvider } from "../hooks/useProfileUser";
 import { REFERENCE_LIST_ITEM_TEST_ID } from "./ReferenceListItem";
 import References from "./References";
 
-const getUserMock = service.user.getUser as MockedService<
-  typeof service.user.getUser
+const getLiteUsersMock = service.user.getLiteUsers as MockedService<
+  typeof service.user.getLiteUsers
 >;
 const getReferencesReceivedMock = service.references
   .getReferencesReceivedForUser as MockedService<
@@ -57,7 +57,7 @@ const [
 
 describe("References", () => {
   beforeEach(() => {
-    getUserMock.mockImplementation(getUser);
+    getLiteUsersMock.mockImplementation(getLiteUsers);
     getReferencesReceivedMock.mockResolvedValue({
       nextPageToken: "",
       referencesList: [friendReference, guestReference1, guestReference2],
@@ -85,7 +85,7 @@ describe("References", () => {
 
     // References received
     for (let i = 0; i < 3; i++) {
-      const user = await getUser(references[i].fromUserId.toString());
+      const user = await getLiteUser(references[i].fromUserId.toString());
       const referenceType = references[i].referenceType as ReferenceType;
       const reference = within(referenceListItems[i]);
 
@@ -183,7 +183,7 @@ describe("References", () => {
 
       references.forEach(async (referenceElement, i) => {
         const reference = within(referenceElement);
-        const user = await getUser(referencesList[i].fromUserId.toString());
+        const user = await getLiteUser(referencesList[i].fromUserId.toString());
         expect(reference.getByRole("heading")).toHaveTextContent(user.name);
         expect(reference.getByText(referencesList[i].text)).toBeVisible();
         // Reference type badge
diff --git a/app/web/features/userQueries/useLiteUsers.test.tsx b/app/web/features/userQueries/useLiteUsers.test.tsx
new file mode 100644
index 0000000000..4aba06b1d0
--- /dev/null
+++ b/app/web/features/userQueries/useLiteUsers.test.tsx
@@ -0,0 +1,432 @@
+import { renderHook } from "@testing-library/react-hooks";
+import { reactQueryRetries } from "appConstants";
+import { liteUserKey, liteUsersKey } from "features/queryKeys";
+import { RpcError, StatusCode } from "grpc-web";
+import { QueryClient, QueryClientProvider } from "react-query";
+import { service } from "service";
+import mockLiteUsers from "test/fixtures/liteUsers.json";
+import { getLiteUser, getLiteUsers } from "test/serviceMockDefaults";
+import { mockConsoleError } from "test/utils";
+
+import { userStaleTime } from "./constants";
+import { useLiteUser, useLiteUsers } from "./useLiteUsers";
+
+jest.mock("service");
+
+const queryClient = new QueryClient({
+  defaultOptions: {
+    queries: {
+      staleTime: userStaleTime,
+      retry: (failureCount, error) => {
+        // don't retry if the user isn't found
+        return (
+          (error as RpcError).code !== StatusCode.NOT_FOUND &&
+          failureCount < reactQueryRetries
+        );
+      },
+    },
+  },
+});
+
+const wrapper = ({ children }: { children: React.ReactNode }) => {
+  return (
+    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
+  );
+};
+
+describe("useLiteUsers & useLiteUser", () => {
+  describe("useLiteUsers", () => {
+    const mockGetLiteUsers = service.user.getLiteUsers as jest.Mock;
+
+    const ids = [1, 2, 3, 4, 5];
+    const mockLiteUsersMap = new Map(
+      mockLiteUsers.map((user) => [user.userId, user])
+    );
+
+    beforeEach(() => {
+      jest.clearAllMocks();
+      queryClient.clear();
+    });
+
+    it("Should return loading state correctly", async () => {
+      const { result, waitFor } = renderHook(() => useLiteUsers(ids), {
+        wrapper,
+      });
+
+      expect(result.current.isLoading).toBe(true);
+      expect(result.current.data).toBeUndefined();
+      await waitFor(() => expect(result.current.isLoading).toBe(false));
+    });
+
+    it("Should return users map when loading is complete", async () => {
+      mockGetLiteUsers.mockResolvedValue(getLiteUsers(ids));
+
+      const { result, waitForNextUpdate } = renderHook(
+        () => useLiteUsers(ids),
+        {
+          wrapper,
+        }
+      );
+
+      await waitForNextUpdate();
+      expect(mockGetLiteUsers).toHaveBeenCalledTimes(1);
+
+      expect(result.current).toEqual(
+        expect.objectContaining({
+          data: mockLiteUsersMap,
+          error: null,
+          isError: false,
+          isFetching: false,
+          isLoading: false,
+          isRefetching: false,
+        })
+      );
+    });
+
+    it("Should filter out undefined ids", async () => {
+      const idsWithUndefined = [1, undefined, 3];
+
+      const { result, waitFor } = renderHook(
+        () => useLiteUsers(idsWithUndefined),
+        { wrapper }
+      );
+
+      await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+      expect(mockGetLiteUsers).toHaveBeenCalledWith([1, 3]);
+    });
+
+    it("Should return an error if the query fails", async () => {
+      mockConsoleError();
+      const error = new RpcError(500, "Some error", {});
+      mockGetLiteUsers.mockRejectedValue(error);
+
+      const { result, waitForNextUpdate } = renderHook(
+        () => useLiteUsers([1, 2, 3]),
+        { wrapper }
+      );
+
+      // Wait for the initial hook to load the data and then fail
+      await waitForNextUpdate(); // First attempt
+      await waitForNextUpdate(); // One retry allowed
+
+      expect(result.current.isLoading).toBe(false);
+      expect(result.current.error).toBe(error);
+      expect(result.current.data).toBeUndefined();
+    });
+
+    it("Should not run the query when ids is an empty array", async () => {
+      const ids: number[] = [];
+
+      renderHook(() => useLiteUsers(ids), { wrapper });
+
+      // Ensure the query function was NOT called
+      expect(mockGetLiteUsers).not.toHaveBeenCalled();
+    });
+
+    it("Should not run the query function when ids is undefined", async () => {
+      const ids: (number | undefined)[] = [undefined];
+
+      const { result, waitFor } = renderHook(() => useLiteUsers(ids), {
+        wrapper,
+      });
+
+      await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+      expect(mockGetLiteUsers).not.toHaveBeenCalled();
+    });
+
+    it("Should return cached data and not call getLiteUsers again if data is fresh", async () => {
+      mockGetLiteUsers.mockResolvedValueOnce(getLiteUsers(ids));
+
+      // Render the hook for the first time
+      const { result, waitForNextUpdate, rerender } = renderHook(
+        () => useLiteUsers(ids),
+        {
+          wrapper,
+        }
+      );
+
+      // Wait for the initial hook to load the data
+      await waitForNextUpdate();
+
+      // Check that the hook has the expected data and `getLiteUsers` was called
+      expect(result.current.data).toBeDefined();
+      expect(result.current.data).toEqual(mockLiteUsersMap);
+      expect(mockGetLiteUsers).toHaveBeenCalledTimes(1);
+
+      // Rerender the hook with the same query key (should use cached data)
+      rerender();
+
+      // Ensure that `getLiteUsers` is not called again, confirming cache usage
+      expect(service.user.getLiteUsers).toHaveBeenCalledTimes(1); // No new fetch should occur
+      expect(result.current.data).toEqual(mockLiteUsersMap);
+      expect(result.current.isFetching).toBe(false);
+      expect(result.current.isLoading).toBe(false);
+    });
+
+    it("Should refetch data if it is stale", async () => {
+      mockGetLiteUsers.mockResolvedValueOnce(getLiteUsers(ids));
+
+      // Render the hook with the wrapper
+      const { result, waitForNextUpdate, rerender } = renderHook(
+        () => useLiteUsers(ids),
+        { wrapper }
+      );
+
+      // Wait for the initial fetch to complete
+      await waitForNextUpdate();
+
+      // Confirm that initial data was loaded
+      expect(result.current.isLoading).toBe(false);
+      expect(result.current.isStale).toBe(false);
+      expect(result.current.data).toEqual(mockLiteUsersMap);
+      expect(mockGetLiteUsers).toHaveBeenCalledTimes(1);
+
+      // Mock a new response for the refetch
+      mockGetLiteUsers.mockResolvedValueOnce(getLiteUsers(ids));
+
+      // Invalidate the query to mark the data as stale
+      queryClient.invalidateQueries(liteUsersKey(ids));
+
+      // Trigger a rerender that should cause a refetch due to stale data
+      rerender();
+
+      // Confirm that initial data is marked as stale and refetching
+      expect(result.current.isStale).toBe(true);
+      expect(result.current.isRefetching).toBe(true);
+
+      // Wait for the refetch to complete
+      await waitForNextUpdate();
+
+      // Confirm that the data has been updated after the refetch
+      expect(result.current.isStale).toBe(false);
+      expect(result.current.data).toEqual(mockLiteUsersMap);
+      expect(mockGetLiteUsers).toHaveBeenCalledTimes(2);
+    });
+
+    it("Should use cached data if subsequent queries fail", async () => {
+      mockGetLiteUsers.mockResolvedValueOnce(getLiteUsers(ids));
+
+      // Render the hook with the wrapper
+      const { result, waitForNextUpdate, rerender } = renderHook(
+        () => useLiteUsers(ids),
+        { wrapper }
+      );
+
+      // Wait for the initial fetch to complete
+      await waitForNextUpdate();
+
+      // Confirm that initial data was loaded
+      expect(result.current.isLoading).toBe(false);
+      expect(result.current.isStale).toBe(false);
+      expect(result.current.data).toEqual(mockLiteUsersMap);
+      expect(mockGetLiteUsers).toHaveBeenCalledTimes(1);
+
+      // Mock a new response for the refetch
+      mockGetLiteUsers.mockRejectedValue(new RpcError(500, "Some error", {}));
+
+      // Invalidate the query to mark the data as stale
+      queryClient.invalidateQueries(liteUsersKey(ids));
+
+      // Trigger a rerender that should cause a refetch due to stale data
+      rerender();
+
+      // Confirm that initial data is marked as stale and refetching
+      expect(result.current.isStale).toBe(true);
+      expect(result.current.isRefetching).toBe(true);
+
+      // Wait for the refetch to complete
+      await waitForNextUpdate(); // first failed attempt
+      await waitForNextUpdate(); // second failed attempt
+
+      // Confirm that the stale data from cache has been returned and isError is true
+      expect(result.current.isStale).toBe(true);
+      expect(result.current.isError).toBe(true);
+      expect(result.current.data).toEqual(mockLiteUsersMap);
+      expect(mockGetLiteUsers).toHaveBeenCalledTimes(3);
+    });
+  });
+
+  describe("useLiteUser (singular)", () => {
+    const mockGetLiteUser = service.user.getLiteUser as jest.Mock;
+
+    beforeEach(() => {
+      jest.clearAllMocks();
+      queryClient.clear();
+    });
+
+    it("Should return loading state correctly", async () => {
+      mockGetLiteUser.mockResolvedValue(getLiteUser("1"));
+
+      const { result, waitFor } = renderHook(() => useLiteUser(1), { wrapper });
+
+      expect(result.current.isLoading).toBe(true);
+      expect(result.current.data).toBeUndefined();
+
+      await waitFor(() => expect(result.current.isLoading).toBe(false));
+    });
+
+    it("Should return user when loading is complete", async () => {
+      mockGetLiteUser.mockResolvedValue(getLiteUser("2"));
+
+      const { result, waitForNextUpdate } = renderHook(() => useLiteUser(1), {
+        wrapper,
+      });
+
+      await waitForNextUpdate();
+
+      expect(result.current.isLoading).toBe(false);
+      expect(mockGetLiteUser).toHaveBeenCalledTimes(1);
+
+      expect(result.current).toEqual(
+        expect.objectContaining({
+          data: mockLiteUsers[1],
+          error: null,
+          isError: false,
+          isFetching: false,
+          isLoading: false,
+          isRefetching: false,
+        })
+      );
+    });
+
+    it("Should return an error if the query fails", async () => {
+      mockConsoleError();
+      const error = new RpcError(500, "Some error", {});
+      mockGetLiteUser.mockRejectedValue(error);
+
+      const { result, waitForNextUpdate } = renderHook(() => useLiteUser(1), {
+        wrapper,
+      });
+
+      await waitForNextUpdate(); // First attempt
+      await waitForNextUpdate(); // One retry allowed
+
+      expect(result.current.isLoading).toBe(false);
+      expect(result.current.error).toBe(error);
+      expect(result.current.isError).toBe(true);
+      expect(result.current.data).toBeUndefined();
+    });
+
+    it("Should not run the query function when id is undefined", async () => {
+      const { result, waitFor } = renderHook(() => useLiteUser(undefined), {
+        wrapper,
+      });
+
+      await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+      expect(mockGetLiteUser).not.toHaveBeenCalled();
+    });
+
+    it("Should return cached data and not call getLiteUser again if data is fresh", async () => {
+      mockGetLiteUser.mockResolvedValueOnce(getLiteUser("1"));
+
+      // Render the hook for the first time
+      const { result, waitForNextUpdate, rerender } = renderHook(
+        () => useLiteUser(1),
+        {
+          wrapper,
+        }
+      );
+
+      // Wait for the initial hook to load the data
+      await waitForNextUpdate();
+
+      // Check that the hook has the expected data and `getLiteUser` was called
+      expect(result.current.data).toEqual(mockLiteUsers[0]);
+      expect(mockGetLiteUser).toHaveBeenCalledTimes(1);
+
+      // Rerender the hook with the same query key (should use cached data)
+      rerender();
+
+      // Ensure that `getLiteUser` is not called again, confirming cache usage
+      expect(mockGetLiteUser).toHaveBeenCalledTimes(1); // No new fetch should occur
+      expect(result.current.data).toEqual(mockLiteUsers[0]);
+      expect(result.current.isFetching).toBe(false);
+      expect(result.current.isLoading).toBe(false);
+    });
+
+    it("Should refetch data if it is stale", async () => {
+      mockGetLiteUser.mockResolvedValueOnce(getLiteUser("2"));
+
+      // Render the hook with the wrapper
+      const { result, waitForNextUpdate, rerender } = renderHook(
+        () => useLiteUser(2),
+        { wrapper }
+      );
+
+      // Wait for the initial fetch to complete
+      await waitForNextUpdate();
+
+      // Confirm that initial data was loaded
+      expect(result.current.isLoading).toBe(false);
+      expect(result.current.isStale).toBe(false);
+      expect(result.current.data).toEqual(mockLiteUsers[1]);
+      expect(mockGetLiteUser).toHaveBeenCalledTimes(1);
+
+      // Mock a new response for the refetch
+      mockGetLiteUser.mockResolvedValueOnce(getLiteUser("2"));
+
+      // Invalidate the query to mark the data as stale
+      queryClient.invalidateQueries(liteUserKey(2));
+
+      // Trigger a rerender that should cause a refetch due to stale data
+      rerender();
+
+      // Confirm that initial data is marked as stale and refetching
+      expect(result.current.isStale).toBe(true);
+      expect(result.current.isRefetching).toBe(true);
+
+      // Wait for the refetch to complete
+      await waitForNextUpdate();
+
+      // Confirm that the data has been updated after the refetch
+      expect(result.current.isStale).toBe(false);
+      expect(result.current.data).toEqual(mockLiteUsers[1]);
+      expect(mockGetLiteUser).toHaveBeenCalledTimes(2);
+    });
+
+    it("Should use cached data if subsequent queries fail", async () => {
+      mockGetLiteUser.mockResolvedValueOnce(getLiteUser("1"));
+
+      // Render the hook with the wrapper
+      const { result, waitForNextUpdate, rerender } = renderHook(
+        () => useLiteUser(1),
+        { wrapper }
+      );
+
+      // Wait for the initial fetch to complete
+      await waitForNextUpdate();
+
+      // Confirm that initial data was loaded
+      expect(result.current.isLoading).toBe(false);
+      expect(result.current.isStale).toBe(false);
+      expect(result.current.data).toEqual(mockLiteUsers[0]);
+      expect(mockGetLiteUser).toHaveBeenCalledTimes(1);
+
+      // Mock a new response for the refetch
+      mockGetLiteUser.mockRejectedValue(new RpcError(500, "Some error", {}));
+
+      // Invalidate the query to mark the data as stale
+      queryClient.invalidateQueries(liteUserKey(1));
+
+      // Trigger a rerender that should cause a refetch due to stale data
+      rerender();
+
+      // Confirm that initial data is marked as stale and refetching
+      expect(result.current.isStale).toBe(true);
+      expect(result.current.isRefetching).toBe(true);
+
+      // Wait for the refetch to complete
+      await waitForNextUpdate();
+      await waitForNextUpdate();
+
+      // Confirm that the stale data from cache has been returned and isError is true
+      expect(result.current.isStale).toBe(true);
+      expect(result.current.isError).toBe(true);
+      expect(result.current.data).toEqual(mockLiteUsers[0]);
+      expect(mockGetLiteUser).toHaveBeenCalledTimes(3);
+    });
+  });
+});
diff --git a/app/web/test/fixtures/liteUsers.json b/app/web/test/fixtures/liteUsers.json
new file mode 100644
index 0000000000..585673a831
--- /dev/null
+++ b/app/web/test/fixtures/liteUsers.json
@@ -0,0 +1,62 @@
+[
+  {
+    "name": "Funny Cat current User",
+    "userId": 1,
+    "username": "funnycat",
+    "city": "Los Angeles",
+    "lat": 34.04,
+    "lng": -118.244349,
+    "radius": 200,
+    "age": 28,
+    "avatarUrl": "https://loremflickr.com/200/200?user1",
+    "avatarThumbnailUrl": "https://loremflickr.com/50/50?user1"
+  },
+  {
+    "name": "Funny Dog",
+    "userId": 2,
+    "username": "funnydog",
+    "city": "Helsinki, Finland",
+    "lat": 60.16,
+    "lng": 24.94,
+    "radius": 200,
+    "age": 35,
+    "avatarUrl": "",
+    "avatarThumbnailUrl": ""
+  },
+  {
+    "name": "Funny Kid",
+    "userId": 3,
+    "username": "funnykid",
+    "city": "London, UK",
+    "lat": 51.5,
+    "lng": -0.11,
+    "radius": 200,
+    "age": 28,
+    "avatarUrl": "https://loremflickr.com/200/200?user3",
+    "avatarThumbnailUrl": "https://loremflickr.com/50/50?user3"
+  },
+  {
+    "name": "Funny Chicken",
+    "userId": 4,
+    "username": "funnyChicken",
+    "city": "Melbourne, Australia",
+    "lat": -37.8,
+    "lng": 144.9,
+    "radius": 200,
+    "age": 28,
+    "avatarUrl": "https://loremflickr.com/200/200?user4",
+    "avatarThumbnailUrl": "https://loremflickr.com/50/50?user4"
+  },
+  {
+    "name": "Friendly Cow",
+    "userId": 5,
+    "username": "friendlyCow",
+    "city": "Melbourne, Australia",
+    "lat": -37.8,
+    "lng": 144.9,
+    "radius": 200,
+    "age": 25,
+    "avatarUrl": "https://loremflickr.com/200/200?user5",
+    "avatarThumbnailUrl": "https://loremflickr.com/50/50?user5"
+  }
+]
diff --git a/app/web/test/serviceMockDefaults.ts b/app/web/test/serviceMockDefaults.ts
index aa4cd9e585..39320c40f5 100644
--- a/app/web/test/serviceMockDefaults.ts
+++ b/app/web/test/serviceMockDefaults.ts
@@ -1,4 +1,4 @@
-import { User } from "proto/api_pb";
+import { GetLiteUsersRes, LiteUser, User } from "proto/api_pb";
 import { ListAdminsRes } from "proto/communities_pb";
 import { HostRequestStatus } from "proto/conversations_pb";
 import { ListEventAttendeesRes, ListEventOrganizersRes } from "proto/events_pb";
@@ -7,10 +7,12 @@ import {
   ReferenceType,
 } from "proto/references_pb";
 import comments from "test/fixtures/comments.json";
+import liteUsers from "test/fixtures/liteUsers.json";
 import messages from "test/fixtures/messages.json";
 import users from "test/fixtures/users.json";
 
 const [user1, user2, user3, user4, user5] = users;
+const [liteUser1, liteUser2, liteUser3, liteUser4, liteUser5] = liteUsers;
 
 const userMap: Record<string, User.AsObject> = {
   "1": user1,
@@ -24,10 +26,38 @@ const userMap: Record<string, User.AsObject> = {
   funnykid: user3,
 };
 
+const liteUserMap: Record<string, LiteUser.AsObject> = {
+  "1": liteUser1,
+  "2": liteUser2,
+  "3": liteUser3,
+  "4": liteUser4,
+  "5": liteUser5,
+  funnycat: liteUser1,
+  funnyChicken: liteUser4,
+  funnydog: liteUser2,
+  funnykid: liteUser3,
+};
+
 export async function getUser(userId: string): Promise<User.AsObject> {
   return userMap[userId];
 }
 
+export async function getLiteUser(userId: string): Promise<LiteUser.AsObject> {
+  return liteUserMap[userId];
+}
+
+export async function getLiteUsers(
+  ids: number[]
+): Promise<GetLiteUsersRes.AsObject> {
+  return {
+    responsesList: ids.map((id) => ({
+      query: "",
+      user: liteUserMap[id.toString()],
+      notFound: false,
+    })),
+  };
+}
+
 export async function listFriends() {
   const [, user2, user3] = users;
   return [user2.userId, user3.userId];