Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 11 commits into from
Jan 6, 2025
14 changes: 9 additions & 5 deletions client/src/components/Chat/ChatView.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
});

Expand Down
20 changes: 15 additions & 5 deletions client/src/components/Chat/Menus/HeaderNewChat.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 />
Expand Down
43 changes: 15 additions & 28 deletions client/src/components/Chat/Messages/MessageIcon.tsx
Original file line number Diff line number Diff line change
@@ -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 = '';

Expand All @@ -30,32 +27,22 @@ 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(
() => getEndpointField(endpointsConfig, endpoint, 'iconURL'),
[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}
Expand All @@ -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}
Expand Down
32 changes: 24 additions & 8 deletions client/src/components/Chat/Messages/MessageParts.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Chat/Messages/MessagesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function MessagesView({
</div>
) : (
<>
{Header && Header}
{Header != null && Header}
<div ref={screenshotTargetRef}>
<MultiMessage
key={conversationId} // avoid internal state mixture
Expand Down
17 changes: 15 additions & 2 deletions client/src/components/Chat/Messages/SearchMessage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
}
Expand All @@ -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 (
Expand All @@ -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>
Expand Down
25 changes: 23 additions & 2 deletions client/src/components/Chat/Messages/ui/MessageRender.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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>
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/Endpoints/ConvoIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export default function ConvoIcon({
<>
{iconURL && iconURL.includes('http') ? (
<ConvoIconURL
preset={conversation}
iconURL={iconURL}
modelLabel={conversation?.chatGptLabel ?? conversation?.modelLabel ?? ''}
endpointIconURL={endpointIconURL}
assistantAvatar={avatar}
assistantName={name}
Expand Down
10 changes: 5 additions & 5 deletions client/src/components/Endpoints/ConvoIconURL.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { memo } from 'react';
import type { TPreset } from 'librechat-data-provider';
import type { IconMapProps } from '~/common';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';

interface ConvoIconURLProps {
preset: TPreset | null;
iconURL?: string;
modelLabel?: string;
endpointIconURL?: string;
assistantName?: string;
agentName?: string;
Expand All @@ -29,15 +29,15 @@ const styleImageMap = {
};

const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
preset,
iconURL = '',
modelLabel = '',
endpointIconURL,
assistantAvatar,
assistantName,
agentAvatar,
agentName,
context,
}) => {
const { iconURL = '' } = preset ?? {};
let Icon: (
props: IconMapProps & {
context?: string;
Expand All @@ -57,7 +57,7 @@ const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
>
<img
src={iconURL}
alt={preset?.chatGptLabel ?? preset?.modelLabel ?? ''}
alt={modelLabel}
style={styleImageMap[context ?? 'default'] ?? styleImageMap.default}
className="object-cover"
/>
Expand Down
6 changes: 2 additions & 4 deletions client/src/components/Endpoints/EndpointIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,8 @@ export default function EndpointIcon({
if (iconURL && (iconURL.includes('http') || iconURL.startsWith('/images/'))) {
return (
<ConvoIconURL
preset={{
...(conversation as TPreset),
iconURL,
}}
iconURL={iconURL}
modelLabel={conversation?.chatGptLabel ?? conversation?.modelLabel ?? ''}
context={context}
endpointIconURL={endpointIconURL}
assistantAvatar={assistantAvatar}
Expand Down
Loading
Loading