refactor: streamline UserAvatar component by optimizing image loading and error handling

This commit is contained in:
Danny Avila 2025-10-26 07:54:32 -04:00
parent 39d83b705b
commit c63f2a634c
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956

View file

@ -1,4 +1,4 @@
import React, { memo, useState, useMemo } from 'react'; import React, { memo, useState, useMemo, useRef, useCallback } from 'react';
import { UserIcon, useAvatar } from '@librechat/client'; import { UserIcon, useAvatar } from '@librechat/client';
import type { TUser } from 'librechat-data-provider'; import type { TUser } from 'librechat-data-provider';
import type { IconProps } from '~/common'; import type { IconProps } from '~/common';
@ -15,20 +15,10 @@ type UserAvatarProps = {
className?: string; className?: string;
}; };
const UserAvatar = memo(({ size, user, avatarSrc, username, className }: UserAvatarProps) => { /**
const [imageError, setImageError] = useState(false); * Default avatar component - memoized outside to prevent recreation on every render
const [imageLoaded, setImageLoaded] = useState(false); */
const DefaultAvatar = memo(() => (
const handleImageError = () => {
setImageError(true);
setImageLoaded(false);
};
const handleImageLoad = () => {
setImageLoaded(true);
};
const renderDefaultAvatar = () => (
<div <div
style={{ style={{
backgroundColor: 'rgb(121, 137, 255)', backgroundColor: 'rgb(121, 137, 255)',
@ -40,19 +30,34 @@ const UserAvatar = memo(({ size, user, avatarSrc, username, className }: UserAva
> >
<UserIcon /> <UserIcon />
</div> </div>
); ));
DefaultAvatar.displayName = 'DefaultAvatar';
const UserAvatar = memo(({ size, user, avatarSrc, username, className }: UserAvatarProps) => {
const [imageError, setImageError] = useState(false);
const imageLoadedRef = useRef(false);
const currentSrcRef = useRef('');
const hasAvatar = useMemo(() => (user?.avatar ?? '') || avatarSrc, [user?.avatar, avatarSrc]);
const showImage = useMemo(() => hasAvatar && !imageError, [hasAvatar, imageError]);
const imageSrc = useMemo(() => (user?.avatar ?? '') || avatarSrc, [user?.avatar, avatarSrc]); const imageSrc = useMemo(() => (user?.avatar ?? '') || avatarSrc, [user?.avatar, avatarSrc]);
/** Check if we're using a custom avatar (not initials) */ /** Reset loaded state if image source changes */
const isCustomAvatar = useMemo(() => !!(user?.avatar && user.avatar !== ''), [user?.avatar]); if (currentSrcRef.current !== imageSrc) {
// /** Only show default while loading for custom avatars, not for initials */ imageLoadedRef.current = false;
// const showDefaultWhileLoading = useMemo( currentSrcRef.current = imageSrc;
// () => isCustomAvatar && !imageLoaded && showImage, }
// [isCustomAvatar, imageLoaded, showImage],
// ); const handleImageError = useCallback(() => {
setImageError(true);
imageLoadedRef.current = false;
}, []);
const handleImageLoad = useCallback(() => {
imageLoadedRef.current = true;
}, []);
const hasAvatar = useMemo(() => imageSrc !== '', [imageSrc]);
const showImage = useMemo(() => hasAvatar && !imageError, [hasAvatar, imageError]);
return ( return (
<div <div
@ -63,27 +68,16 @@ const UserAvatar = memo(({ size, user, avatarSrc, username, className }: UserAva
}} }}
className={cn('relative flex items-center justify-center', className ?? '')} className={cn('relative flex items-center justify-center', className ?? '')}
> >
{/* Always render default avatar as background */} {!showImage ? (
{!showImage && renderDefaultAvatar()} <DefaultAvatar />
{showImage && ( ) : (
<>
{/* Show default avatar behind the image while loading custom avatars */}
{isCustomAvatar && !imageLoaded && (
<div className="absolute inset-0 flex items-center justify-center">
{renderDefaultAvatar()}
</div>
)}
<img <img
className={cn( className="rounded-full"
'relative z-10 rounded-full',
isCustomAvatar && !imageLoaded ? 'opacity-0' : 'opacity-100',
)}
src={imageSrc} src={imageSrc}
alt="avatar" alt="avatar"
onLoad={handleImageLoad} onLoad={handleImageLoad}
onError={handleImageError} onError={handleImageError}
/> />
</>
)} )}
</div> </div>
); );