🧵 fix: Prevent Unnecessary Re-renders when Loading Chats (#5189)

* chore: typing

* chore: typing

* fix: enhance message scrolling logic to handle empty messages tree and ref checks

* fix: optimize message selection logic with useCallback for better performance

* chore: typing

* refactor: optimize icon rendering

* refactor: further optimize chat props

* fix: remove unnecessary console log in useQueryParams cleanup

* refactor: add queryClient to reset message data on new conversation initiation

* refactor: update data-testid attributes for consistency and improve code readability

* refactor: integrate queryClient to reset message data on new conversation initiation
This commit is contained in:
Danny Avila 2025-01-06 10:32:44 -05:00 committed by GitHub
parent 7987e04a2c
commit b01c744eb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 184 additions and 88 deletions

View file

@ -1,8 +1,9 @@
import { memo } from 'react';
import { memo, useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { useForm } from 'react-hook-form';
import { useParams } from 'react-router-dom';
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
import type { TMessage } from 'librechat-data-provider';
import type { ChatFormValues } from '~/common';
import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers';
import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks';
@ -24,10 +25,13 @@ function ChatView({ index = 0 }: { index?: number }) {
const fileMap = useFileMapContext();
const { data: messagesTree = null, isLoading } = useGetMessagesByConvoId(conversationId ?? '', {
select: (data) => {
const dataTree = buildTree({ messages: data, fileMap });
return dataTree?.length === 0 ? null : dataTree ?? null;
},
select: useCallback(
(data: TMessage[]) => {
const dataTree = buildTree({ messages: data, fileMap });
return dataTree?.length === 0 ? null : dataTree ?? null;
},
[fileMap],
),
enabled: !!fileMap,
});

View file

@ -1,9 +1,13 @@
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
import { useMediaQuery, useLocalize } from '~/hooks';
import { NewChatIcon } from '~/components/svg';
import { useChatContext } from '~/Providers';
import { useMediaQuery, useLocalize } from '~/hooks';
export default function HeaderNewChat() {
const { newConversation } = useChatContext();
const queryClient = useQueryClient();
const { conversation, newConversation } = useChatContext();
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const localize = useLocalize();
if (isSmallScreen) {
@ -12,10 +16,16 @@ export default function HeaderNewChat() {
return (
<button
data-testid="wide-header-new-chat-button"
aria-label={localize("com_ui_new_chat")}
aria-label={localize('com_ui_new_chat')}
type="button"
className="btn btn-neutral btn-small border-token-border-medium relative ml-2 flex hidden h-9 w-9 items-center justify-center whitespace-nowrap rounded-lg rounded-lg border focus:border-black-500 dark:focus:border-white-500 md:flex"
onClick={() => newConversation()}
className="btn btn-neutral btn-small border-token-border-medium focus:border-black-500 dark:focus:border-white-500 relative ml-2 flex h-9 w-9 items-center justify-center whitespace-nowrap rounded-lg border md:flex"
onClick={() => {
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
newConversation();
}}
>
<div className="flex w-full items-center justify-center gap-2">
<NewChatIcon />

View file

@ -1,26 +1,23 @@
import React, { useMemo, memo } from 'react';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TMessage, TPreset, Assistant, Agent } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
import type { Assistant, Agent, TMessage } from 'librechat-data-provider';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { getEndpointField, getIconEndpoint } from '~/utils';
import Icon from '~/components/Endpoints/Icon';
const MessageIcon = memo(
(
props: Pick<TMessageProps, 'message' | 'conversation'> & {
assistant?: Assistant;
agent?: Agent;
},
) => {
(props: {
iconData?: TMessage & { modelLabel?: string };
assistant?: Assistant;
agent?: Agent;
}) => {
const { data: endpointsConfig } = useGetEndpointsQuery();
const { message, conversation, assistant, agent } = props;
const { iconData, assistant, agent } = props;
const assistantName = useMemo(() => assistant?.name ?? '', [assistant]);
const assistantAvatar = useMemo(() => assistant?.metadata?.avatar ?? '', [assistant]);
const agentName = useMemo(() => props.agent?.name ?? '', [props.agent]);
const agentAvatar = useMemo(() => props.agent?.avatar?.filepath ?? '', [props.agent]);
const isCreatedByUser = useMemo(() => message?.isCreatedByUser ?? false, [message]);
let avatarURL = '';
@ -30,21 +27,10 @@ const MessageIcon = memo(
avatarURL = agentAvatar;
}
const messageSettings = useMemo(
() => ({
...(conversation ?? {}),
...({
...(message ?? {}),
iconURL: message?.iconURL ?? '',
} as TMessage),
}),
[conversation, message],
);
const iconURL = messageSettings.iconURL;
const iconURL = iconData?.iconURL;
const endpoint = useMemo(
() => getIconEndpoint({ endpointsConfig, iconURL, endpoint: messageSettings.endpoint }),
[endpointsConfig, iconURL, messageSettings.endpoint],
() => getIconEndpoint({ endpointsConfig, iconURL, endpoint: iconData?.endpoint }),
[endpointsConfig, iconURL, iconData?.endpoint],
);
const endpointIconURL = useMemo(
@ -52,10 +38,11 @@ const MessageIcon = memo(
[endpointsConfig, endpoint],
);
if (isCreatedByUser !== true && iconURL != null && iconURL.includes('http')) {
if (iconData?.isCreatedByUser !== true && iconURL != null && iconURL.includes('http')) {
return (
<ConvoIconURL
preset={messageSettings as typeof messageSettings & TPreset}
iconURL={iconURL}
modelLabel={iconData?.modelLabel}
context="message"
assistantAvatar={assistantAvatar}
agentAvatar={agentAvatar}
@ -68,10 +55,10 @@ const MessageIcon = memo(
return (
<Icon
isCreatedByUser={isCreatedByUser}
isCreatedByUser={iconData?.isCreatedByUser ?? false}
endpoint={endpoint}
iconURL={avatarURL || endpointIconURL}
model={message?.model ?? conversation?.model}
model={iconData?.model}
assistantName={assistantName}
agentName={agentName}
size={28.8}

View file

@ -1,7 +1,8 @@
import React, { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import type { TMessageContentParts } from 'librechat-data-provider';
import type { TMessage, TMessageContentParts } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
import Icon from '~/components/Chat/Messages/MessageIcon';
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import { useMessageHelpers, useLocalize } from '~/hooks';
import ContentParts from './Content/ContentParts';
import SiblingSwitch from './SiblingSwitch';
@ -35,6 +36,26 @@ export default function Message(props: TMessageProps) {
const fontSize = useRecoilValue(store.fontSize);
const { children, messageId = null, isCreatedByUser } = message ?? {};
const iconData = useMemo(
() =>
({
endpoint: conversation?.endpoint,
model: conversation?.model ?? message?.model,
iconURL: conversation?.iconURL ?? message?.iconURL ?? '',
modelLabel: conversation?.chatGptLabel ?? conversation?.modelLabel,
isCreatedByUser: message?.isCreatedByUser,
} as TMessage & { modelLabel?: string }),
[
conversation?.chatGptLabel,
conversation?.modelLabel,
conversation?.endpoint,
conversation?.iconURL,
conversation?.model,
message?.model,
message?.iconURL,
message?.isCreatedByUser,
],
);
if (!message) {
return null;
}
@ -62,12 +83,7 @@ export default function Message(props: TMessageProps) {
<div>
<div className="pt-0.5">
<div className="shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon
message={message}
conversation={conversation}
assistant={assistant}
agent={agent}
/>
<MessageIcon iconData={iconData} assistant={assistant} agent={agent} />
</div>
</div>
</div>

View file

@ -56,7 +56,7 @@ export default function MessagesView({
</div>
) : (
<>
{Header && Header}
{Header != null && Header}
<div ref={screenshotTargetRef}>
<MultiMessage
key={conversationId} // avoid internal state mixture

View file

@ -1,5 +1,7 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { useAuthContext, useLocalize } from '~/hooks';
import type { TMessage } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons';
import Icon from '~/components/Chat/Messages/MessageIcon';
@ -15,6 +17,17 @@ export default function Message({ message }: Pick<TMessageProps, 'message'>) {
const { user } = useAuthContext();
const localize = useLocalize();
const iconData = useMemo(
() =>
({
endpoint: message?.endpoint,
model: message?.model,
iconURL: message?.iconURL ?? '',
isCreatedByUser: message?.isCreatedByUser,
} as TMessage & { modelLabel?: string }),
[message?.model, message?.iconURL, message?.endpoint, message?.isCreatedByUser],
);
if (!message) {
return null;
}
@ -27,7 +40,7 @@ export default function Message({ message }: Pick<TMessageProps, 'message'>) {
? (user?.name ?? '') || (user?.username ?? '')
: localize('com_user_message');
} else {
messageLabel = message.sender || '';
messageLabel = message.sender ?? '';
}
return (
@ -39,7 +52,7 @@ export default function Message({ message }: Pick<TMessageProps, 'message'>) {
<div>
<div className="pt-0.5">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon message={message} />
<Icon iconData={iconData} />
</div>
</div>
</div>

View file

@ -6,7 +6,7 @@ import MessageContent from '~/components/Chat/Messages/Content/MessageContent';
import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow';
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import Icon from '~/components/Chat/Messages/MessageIcon';
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow';
import { MessageContext } from '~/Providers';
@ -66,6 +66,27 @@ const MessageRender = memo(
[hasNoChildren, msg?.depth, latestMessage?.depth],
);
const iconData = useMemo(
() =>
({
endpoint: conversation?.endpoint,
model: conversation?.model ?? msg?.model,
iconURL: conversation?.iconURL ?? msg?.iconURL ?? '',
modelLabel: conversation?.chatGptLabel ?? conversation?.modelLabel,
isCreatedByUser: msg?.isCreatedByUser,
} as TMessage & { modelLabel?: string }),
[
conversation?.chatGptLabel,
conversation?.modelLabel,
conversation?.endpoint,
conversation?.iconURL,
conversation?.model,
msg?.model,
msg?.iconURL,
msg?.isCreatedByUser,
],
);
if (!msg) {
return null;
}
@ -125,7 +146,7 @@ const MessageRender = memo(
<div>
<div className="pt-0.5">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon message={msg} conversation={conversation} assistant={assistant} />
<MessageIcon iconData={iconData} assistant={assistant} />
</div>
</div>
</div>