DEV Community

Cover image for Building a Custom Hook for Theme-Aware Logo Mapping in Next.js
Sushil
Sushil

Posted on

Building a Custom Hook for Theme-Aware Logo Mapping in Next.js

Building a Custom Hook for Theme-Aware Logo Management in React

Recently, I've been recreating the premium portfolio template from Aceternity UI's Minimal Portfolio Template.

Portfolio Template Preview

The template is modern, simple, and professional. I've recreated the entire website with some additional features like syntax highlighting in blogs and dedicated single pages for projects. And yes, I haven't bought the original template - this is my own implementation inspired by the design.

The Challenge: Professional UI/UX with Theme Support

While building the website, I wanted to ensure a professional UI/UX experience that looks great in both light and dark modes, especially when displaying technology logos.

Initially, I hardcoded the tech stack like this:

export const skills: Skill[] = [
  { name: "JavaScript", logo: "/lang-icons/javascript.svg" },
  { name: "TypeScript", logo: "/lang-icons/typescript.svg" },
  { name: "Node", logo: "/lang-icons/node-logo.svg" },
  { name: "React", logo: "/lang-icons/react.svg" },
  { name: "Next.js", logo: "/lang-icons/nextjs-logo.svg" },
  // etc...
];
Enter fullscreen mode Exit fullscreen mode

This approach works fine for a single theme, but problems arise when you need to support both light and dark modes. Some logos that look great on light backgrounds become invisible on dark ones, and vice versa.

The Solution: Logo Mapping Object

To solve this, I created an object that maps each technology name to both light and dark logo variants:

type LogoVariant = {
  light: string;
  dark: string;
};

export const logoMapper: Record<string, LogoVariant> = {
  "Next.js": {
    light: "/lang-icons/nextjs-black.svg",
    dark: "/lang-icons/next-js-logo-white.webp",
  },
  React: {
    light: "/lang-icons/react.svg",
    dark: "/lang-icons/react.svg",
  },
  MongoDB: {
    light: "/lang-icons/mongodb.svg",
    dark: "/lang-icons/mongodb.svg",
  },
  Node: {
    light: "/lang-icons/node-logo.svg",
    dark: "/lang-icons/node-logo.svg",
  },
  "Tailwind CSS": {
    light: "/lang-icons/tailwind.svg",
    dark: "/lang-icons/tailwind.svg",
  },
  // More technologies...
};
Enter fullscreen mode Exit fullscreen mode

Now I have both variants of each logo, but I still need to handle the theme switching logic.

The Problem: Repetitive Theme Logic

With this setup, I had to use the useTheme hook and write conditional logic everywhere I needed to display a logo:

"use client";

import React from "react";
import { useTheme } from "next-themes";
import { logoMapper } from "@/lib/logoMapper";
import Image from "next/image";

const SingleProjectPage = ({ params }: { params: Promise<{ slug: string }> }) => {
  const { theme } = useTheme();
  const resolvedParams = React.use(params);
  const project = singleProjects.find((p) => p.slug === resolvedParams.slug);

  if (!project) return <div>Project not found</div>;

  return (
    <div className="px-4 md:px-8">
      {/* Project header */}
      <div className="flex flex-col items-start gap-3 px-6">
        <h1>{project.title}</h1>
        <p>{project.shortDescription}</p>
      </div>

      {/* Technologies section */}
      <div className="shadow-section-inset p-4 md:p-6 md:pb-8">
        <h2>Technologies Used</h2>
        <div className="grid w-full grid-cols-2 gap-6 pt-0 md:grid-cols-4">
          {project.technologies.map((tech, idx) => (
            <div key={idx} className="group cursor-default">
              <div className="h-full p-2 transition-all duration-300">
                <div className="flex items-start justify-between">
                  <div className="flex items-center gap-x-4">
                    {logoMapper[tech.name] && (
                      <div className="relative size-5 md:size-7">
                        <Image
                          src={
                            theme === "dark"
                              ? logoMapper[tech.name].dark
                              : logoMapper[tech.name].light
                          }
                          alt={`${tech.name} logo`}
                          fill
                          className="object-contain"
                        />
                      </div>
                    )}
                    <h3>{tech.name}</h3>
                  </div>
                </div>
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

export default SingleProjectPage;
Enter fullscreen mode Exit fullscreen mode

The repetitive part was this conditional logic:

<Image
  src={
    theme === "dark"
      ? logoMapper[tech.name].dark
      : logoMapper[tech.name].light
  }
  alt={`${tech.name} logo`}
  fill
  className="object-contain"
/>
Enter fullscreen mode Exit fullscreen mode

This became tedious and error-prone when used across multiple components.

The Solution: Custom useLogo Hook

To eliminate this repetition, I created a custom hook called useLogo that handles all the theme logic:

"use client";

import { useTheme } from "next-themes";

type LogoVariant = {
  light: string;
  dark: string;
};

export const logoMapper: Record<string, LogoVariant> = {
  "Next.js": {
    light: "/lang-icons/nextjs-black.svg",
    dark: "/lang-icons/next-js-logo-white.webp",
  },
  React: {
    light: "/lang-icons/react.svg",
    dark: "/lang-icons/react.svg",
  },
  MongoDB: {
    light: "/lang-icons/mongodb.svg",
    dark: "/lang-icons/mongodb.svg",
  },
  Node: {
    light: "/lang-icons/node-logo.svg",
    dark: "/lang-icons/node-logo.svg",
  },
  "Tailwind CSS": {
    light: "/lang-icons/tailwind.svg",
    dark: "/lang-icons/tailwind.svg",
  },
  Neon: {
    light: "/lang-icons/neon-dark.svg",
    dark: "/lang-icons/neon-light.svg",
  },
  Cloudinary: {
    light: "/lang-icons/cloudinary.png",
    dark: "/lang-icons/cloudinary.png",
  },
  // Add more technologies as needed
};

export const useLogo = () => {
  const { theme } = useTheme();

  const getLogo = (techName: string): string => {
    const logoVariant = logoMapper[techName];

    if (!logoVariant) {
      console.warn(`Logo not found for technology: ${techName}`);
      return "";
    }

    return theme === "dark" ? logoVariant.dark : logoVariant.light;
  };

  return { getLogo };
};
Enter fullscreen mode Exit fullscreen mode

Using the Custom Hook

Now, using the hook is much cleaner and more maintainable:

"use client";

import { useLogo } from "@/lib/logoMapper";
import Image from "next/image";

const SingleProjectPage = ({ params }: { params: Promise<{ slug: string }> }) => {
  const { getLogo } = useLogo();

  return (
    <div>
      {/* Your component JSX */}
      <Image
        src={getLogo(tech.name)}
        alt={`${tech.name} logo`}
        fill
        className="object-contain"
      />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Benefits of This Approach

1. Clean Code: No more repetitive conditional logic scattered throughout components.

2. Centralized Management: All logo mapping logic lives in one place, making it easy to maintain and update.

3. Error Handling: The hook includes built-in warning messages for missing logos, helping with debugging.

4. Type Safety: Full TypeScript support ensures you catch errors at compile time.

5. Reusability: Use the hook in any component that needs theme-aware logos.

6. Easy Extension: Adding new technologies is as simple as updating the logoMapper object.

Key Features

  • Automatic Theme Detection: The hook automatically detects the current theme and returns the appropriate logo
  • Fallback Handling: Returns an empty string and logs a warning for missing logos
  • Performance: Minimal overhead with efficient theme detection
  • Developer Experience: Simple API with just one function to remember

Conclusion

This custom hook solution transformed my codebase from repetitive, hard-to-maintain conditional logic to a clean, reusable system. It's a perfect example of how custom hooks can solve common React patterns and improve code quality.

The useLogo hook not only makes the code more readable but also ensures consistency across the entire application. When I need to add a new technology or update logo paths, I only need to modify the logoMapper object in one place.

Top comments (0)