DEV Community

cycy
cycy

Posted on

Building a Smart Avatar Component in Next.js with React

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 and height - 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?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Don't Forget Your Default Avatar!

For our backup system to work, you need to create a default profile picture:

  1. Create a folder called assets in your project
  2. Add a picture called default-avatar.png in that folder
  3. 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";
Enter fullscreen mode Exit fullscreen mode

Problem: Profile Pictures Not Loading

Fix: Make sure your default avatar exists at:

/public/assets/default-avatar.png
Enter fullscreen mode Exit fullscreen mode

Problem: TypeScript Errors with src prop

Fix: Use the || undefined pattern when passing possibly null values:

<Avatar src={user.profilePicture || undefined} />
Enter fullscreen mode Exit fullscreen mode

What Makes This Avatar Special?

  1. It Never Breaks - If the main picture fails, it shows a backup
  2. It Has Loading States - Shows a nice animation while images load
  3. It Handles Missing Pictures - Uses a default when no picture exists
  4. It's Reusable - Use it anywhere you need profile pictures
  5. 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)