A beginner-friendly guide to creating a robust profile picture component that handles all the tricky parts automatically
Introduction
Profile pictures are everywhere in apps, but they can be tricky to get right. What happens when the image doesn't load? Or when the user doesn't have a profile picture? Let's build an Avatar component that handles all these problems automatically!
What's Wrong with Just Using <img>
?
Using a regular <img>
tag for profile pictures causes several problems:
- Ugly broken image icons when pictures don't load
- Nothing shows up when users don't have profile pictures
- The layout jumps around while images load
- No pretty loading animations
Our Avatar component will fix all of these issues!
The Complete Code
Here's our full Avatar component:
// components/ui/avatar.tsx
import Image from "next/image";
import { useState, useEffect } from "react";
interface AvatarProps {
src?: string | null;
alt: string;
fallback?: string;
className?: string;
width?: number;
height?: number;
}
export const Avatar: React.FC<AvatarProps> = ({
src,
alt,
fallback = "/assets/default-avatar.png",
className = "",
width = 100,
height = 100,
}) => {
// Initialize state
const [imgSrc, setImgSrc] = useState<string>(src || fallback);
const [hasError, setHasError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
// Use effect to update imgSrc when src prop changes
useEffect(() => {
// Reset error state when src changes
setHasError(false);
setIsLoading(true);
// Set the new image source
if (src) {
setImgSrc(src);
console.log("Avatar: Source updated to:", src);
} else {
setImgSrc(fallback);
console.log("Avatar: Using fallback image:", fallback);
}
}, [src, fallback]); // Re-run when src or fallback changes
// Enhanced error handler
const handleError = () => {
console.error("Avatar: Image failed to load:", imgSrc);
if (!hasError) {
setHasError(true);
setImgSrc(fallback);
}
setIsLoading(false);
};
// Enhanced load handler
const handleLoad = () => {
console.log("Avatar: Image loaded successfully:", imgSrc);
setIsLoading(false);
};
return (
<div className={`relative overflow-hidden rounded-full ${className}`}>
{isLoading && (
<div className="absolute inset-0 bg-gray-200 animate-pulse rounded-full" />
)}
<Image
src={imgSrc}
alt={alt}
width={width}
height={height}
className="object-cover w-full h-full"
onError={handleError}
onLoad={handleLoad}
priority={false}
unoptimized={process.env.NODE_ENV === 'development'}
/>
</div>
);
}
Let's break down this component piece by piece.
1️⃣ The Frame Setup (Component Interface)
interface AvatarProps {
src?: string | null; // The photo you want to show
alt: string; // Description of the photo for blind people
fallback?: string; // A backup photo if the main one is broken
className?: string; // How to decorate the frame
width?: number; // How wide the frame should be
height?: number; // How tall the frame should be
}
This is like explaining what our picture frame needs to work:
-
src
- The address where we can find the picture (optional) -
alt
- Words that describe the picture for people who can't see it -
fallback
- A backup picture to use if the main one is missing -
className
- Special decorations for our frame -
width
andheight
- How big the frame should be
2️⃣ The Frame's Secret Powers (Component States)
const [imgSrc, setImgSrc] = useState<string>(src || fallback); // Which photo to show right now
const [hasError, setHasError] = useState(false); // Did the photo break?
const [isLoading, setIsLoading] = useState(true); // Is the photo still loading?
These are like the frame's secret powers:
-
imgSrc
- The current photo we're showing -
hasError
- Whether the photo got damaged and couldn't be shown -
isLoading
- Whether we're still waiting for the photo to arrive
3️⃣ The Magic Watcher (useEffect)
useEffect(() => {
// This is like having a friend who watches for a new photo
// When you get a new photo, your friend:
// 1. Forgets about any broken photos from before
setHasError(false);
// 2. Says "I'm getting a new photo ready!"
setIsLoading(true);
// 3. Checks if you actually have a photo
if (src) {
// Put the new photo in the frame
setImgSrc(src);
console.log("Avatar: Source updated to:", src);
} else {
// If there's no photo, use the backup photo instead
setImgSrc(fallback);
console.log("Avatar: Using fallback image:", fallback);
}
}, [src, fallback]); // This tells your friend to watch these two things
This is like having a helper friend who watches for new photos:
- When a new photo arrives, they get ready to put it in the frame
- They forget about any broken photos from before
- They tell everyone "I'm working on putting in a new photo!"
- If there's actually a photo, they put it in the frame
- If there's no photo, they use the backup photo instead
- The
[src, fallback]
part tells the friend "only do this work when either the photo or the backup changes"
4️⃣ When Something Goes Wrong (Error Handler)
const handleError = () => {
// If the photo gets torn or damaged:
console.error("Avatar: Image failed to load:", imgSrc);
// Replace it with the backup photo
setHasError(true);
setImgSrc(fallback);
setIsLoading(false); // We're done trying to load it
};
This is what happens if the photo gets damaged:
- We tell the computer "Hey, this photo is broken!"
- We mark the photo as having an error
- We quickly replace it with our backup photo
- We let everyone know we're done trying to load the broken photo
5️⃣ When the Photo Looks Great (Load Handler)
const handleLoad = () => {
// The photo is now showing perfectly!
console.log("Avatar: Image loaded successfully:", imgSrc);
setIsLoading(false); // We've finished loading it
};
This is what happens when the photo loads perfectly:
- We tell the computer "Yay, the photo loaded!"
- We let everyone know we're done loading, and they can see the perfect photo now
6️⃣ Showing the Frame and Photo (Return Statement)
return (
<div className={`relative overflow-hidden rounded-full ${className}`}>
{/* If still loading, show a gray placeholder */}
{isLoading && (
<div className="absolute inset-0 bg-gray-200 animate-pulse rounded-full" />
)}
{/* The actual photo in the frame */}
<Image
src={imgSrc} // Which photo to show
alt={alt} // Description for blind people
width={width} // How wide
height={height} // How tall
className="object-cover w-full h-full" // Make sure it fits nicely
onError={handleError} // What to do if the photo breaks
onLoad={handleLoad} // What to do when the photo loads
priority={false} // Not super important to load first
unoptimized={process.env.NODE_ENV === 'development'} // Special trick for developing
/>
</div>
);
This is the final step where we actually show our picture frame:
- We create a round frame (using
rounded-full
) - If the photo is still loading, we show a pulsing gray circle as a placeholder
- We put the actual photo in the frame
- We tell the frame what to do if the photo breaks
- We tell the frame what to do when the photo loads perfectly
- We make sure the photo fills the frame nicely (with
object-cover
)
How to Use Our Avatar Component
Using our component is super easy! Here's an example:
// In any other component:
import { Avatar } from "@/components/ui/avatar";
function UserProfile({ user }) {
return (
<div className="flex items-center gap-4">
<Avatar
src={user.profilePicture || undefined}
alt={`${user.name}'s profile picture`}
width={48}
height={48}
className="w-12 h-12"
/>
<div>
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
</div>
);
}
Don't Forget Your Default Avatar!
For our backup system to work, you need to create a default profile picture:
- Create a folder called assets in your project
- Add a picture called
default-avatar.png
in that folder - This will be shown whenever a profile picture is missing or broken
Common Problems & Solutions
Problem: Red Squiggly Lines Under useEffect
Fix: Make sure you import it at the top of your file:
import { useState, useEffect } from "react";
Problem: Profile Pictures Not Loading
Fix: Make sure your default avatar exists at:
/public/assets/default-avatar.png
Problem: TypeScript Errors with src
prop
Fix: Use the || undefined
pattern when passing possibly null values:
<Avatar src={user.profilePicture || undefined} />
What Makes This Avatar Special?
- It Never Breaks - If the main picture fails, it shows a backup
- It Has Loading States - Shows a nice animation while images load
- It Handles Missing Pictures - Uses a default when no picture exists
- It's Reusable - Use it anywhere you need profile pictures
- It's TypeScript Safe - Works well with TypeScript's type checking
Conclusion
Now you have a super-smart Avatar component that handles all the tricky parts of showing profile pictures. No more broken images, no more missing pictures, and no more layout jumps when images load!
Use this component anywhere you need to show profile pictures in your app, and they'll always look great.
Want to learn more about building smart components like this? Let me know in the comments!
Top comments (0)