Reusable components play a crucial role in the development process for several reasons. They are versatile building blocks that can be utilized throughout your application, offering benefits such as easier maintenance and bug fixing. When it comes to creating a reusable component for image or link elements, there are various approaches, but it's essential to explore more optimized and effective methods. Let's delve into this by discussing Next.js Image components and how they can be crafted in a more efficient way.
import Image from 'next/image';
interface Props {
width: number,
height: number,
alt: string,
src: string,
styles: string // for styling
// other styling props
}
const NextImage = (props: Props) => {
return (
<Image
src={props.src}
width={props.width}
height={props.height}
alt={props.alt}
className={props.styles}
/>
);
};
export default NextImage;
In the traditional approach, the NextImage
component is created with explicit prop definitions. While functional, it can become cumbersome when you need to modify this component for specific use cases. Making modifications could lead to the creation of multiple similar components, making your codebase less organized.
'use client';
import Image from 'next/image';
import cn from 'clsx';
import { ComponentProps, useState } from 'react';
const NextImage = (props: ComponentProps<typeof Image>) => {
const [isLoading, setLoading] = useState(true);
return (
<Image
{...props}
src={props.src}
priority={true}
className={cn(
props.className,
'duration-700 ease-in-out',
isLoading ? 'scale-105 blur-lg' : 'scale-100 blur-0'
)}
onLoadingComplete={() => setLoading(false)}
/>
);
};
export default NextImage;
The new approach offers a more optimized and efficient way to create a reusable Next.js Image
component. It utilizes the ComponentProps
type, enabling the component to inherit and adapt to a broader range of props. This versatility allows you to make specific modifications as needed without cluttering your codebase with additional components. In addition, you take advantage of Next.js Image component features such as priority
, which optimizes image loading.
To further clarify, the clsx
utility library is used to conditionally apply CSS classes to elements, enhancing the component's styling capabilities. The useState
hook manages the loading state of the image, and you've set up basic styling for the component, including the ability to scale and blur the image based on loading conditions. By adopting this approach, you maintain the flexibility to tailor your Next.js image component while keeping your codebase organized and efficient.
The traditional way of rendering Next.Js links can be less effective and makes modification difficult, as we’ve seen above, so let me code you the code for a more optimized Link Component:
import Link from 'next/link';
import cn from 'clsx';
import { ComponentProps } from 'react';
const NextLink = (props: ComponentProps<typeof Link>) => {
return (
<Link
{...props}
className={cn(
props.className,
`hover:underline font-medium text-black transition-all duration-200 rounded dark:text-gray-300`
)}
href={props.href}
title={props.title}
>
{props.children}
</Link>
);
};
export default NextLink;
Much like the Image Component we discussed earlier, this component comes with basic styling for consistency and usability. However, it offers a more optimized approach to rendering links in your Next.js application. This ensures that your links not only maintain a consistent style but also remain adaptable and easy to modify as your project evolves.
In our pursuit of creating reusable components, it's essential to extend this approach to various other elements like buttons, headers (h1), paragraphs (p), code blocks, or even custom container elements. While we won't be using ComponentProps
this time, we can still use the element's interface. Let's explore this by creating a custom button component:
import cn from "clsx";
import { ButtonHTMLAttributes } from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
className?: string;
}
const Button = (props: ButtonProps) => {
const { className, ...rest } = props;
return (
<button {...rest} className={cn(className)}>
{props.children}
</button>
);
};
export default Button;
As you can see, we've created an interface called ButtonProps
, extending it to inherit all the props from the button attributes. We've also considered the possibility that the className
can be undefined or have custom styling provided by the user. This allows us to create a custom button component that's both versatile and adaptable, ensuring a consistent structure for your buttons while allowing for easy modifications.
When creating web pages, maintaining a structured layout is crucial. To achieve this, you might need to design containers for your content. Similar to what we discussed with the Button Component, we can use a similar approach to create these containers. Let's explore how to create a basic container for your web pages that can be used consistently throughout your application.
Here's an example of a simple yet versatile container:
import cn from "clsx";
import { HTMLAttributes } from "react";
interface SectionProps extends HTMLAttributes<HTMLElement> {
className?: string;
}
const Container = (props: SectionProps) => {
return (
<section
{...props}
className={cn(
props.className,
"px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl bg-white mt-8"
)}
>
{props.children}
</section>
);
};
export default Container;
This Container component, like the previous Button component, doesn't introduce custom props or additional complexities. It simply relies on the className
for styling, making it adaptable and straightforward for use across various parts of your application.
A little Complex Resuable Component
Now, let's consider a more complex scenario where we want to create a dropdown menu or list. In such cases, we need to introduce custom props to handle the menu's visibility. We'll define the showUserDropdown
and setShowUserDropdown
props to manage this.
Here's the initial version of the component:
import cn from "clsx";
import { useEffect, useRef } from "react";
interface DropdownWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
showUserDropdown: boolean;
setShowUserDropdown: React.Dispatch<React.SetStateAction<boolean>>;
}
const DropdownWrapper = (props: DropdownWrapperProps) => {
const userDropdownRef = useRef<HTMLDivElement | null>(null);
const { className, ...rest } = props;
useEffect(() => {
const handleOutsideClick = (event: any) => {
if (
props.showUserDropdown &&
userDropdownRef.current &&
!userDropdownRef.current.contains(event.target)
) {
props.setShowUserDropdown(false);
}
};
document.addEventListener("click", handleOutsideClick);
return () => {
document.removeEventListener("click", handleOutsideClick);
};
}, [props]);
return (
<div {...rest} className={cn(className)} ref={userDropdownRef}>
{props.children}
</div>
);
};
export default DropdownWrapper;
This component is functional but not entirely correct; why? Because we are passing the showUserDropdown
and setShowUserDropdown
to the div as {..rest}
which will either throw an error in the server(terminal) or client(UI). So to fix this, we destruct it alongside className
, then modify props.showUserDropdown
to showUserDropdown
and the same for setShowUserDropdown
.
import cn from "clsx";
import { useEffect, useRef } from "react";
interface DropdownWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
showUserDropdown: boolean;
setShowUserDropdown: React.Dispatch<React.SetStateAction<boolean>>;
}
const DropdownWrapper = (props: DropdownWrapperProps) => {
const userDropdownRef = useRef<HTMLDivElement | null>(null);
const { className, showUserDropdown, setShowUserDropdown, ...rest } = props;
useEffect(() => {
const handleOutsideClick = (event: any) => {
if (
showUserDropdown &&
userDropdownRef.current &&
!userDropdownRef.current.contains(event.target)
) {
setShowUserDropdown(false);
}
};
document.addEventListener("click", handleOutsideClick);
return () => {
document.removeEventListener("click", handleOutsideClick);
};
}, [props]);
return (
<div {...rest} className={cn(className)} ref={userDropdownRef}>
{props.children}
</div>
);
};
export default DropdownWrapper;
By making this adjustment, the component functions correctly, ensuring the showUserDropdown
and setShowUserDropdown
props are used in a way that won't cause issues in either the server or the client-side rendering.
In this exploration of creating reusable components and handling more complex elements in React, we've seen how to enhance the efficiency and maintainability of your code. By designing components like buttons, containers, and dropdowns, we've shown how you can structure your application for consistency while keeping the flexibility to adapt and modify as needed.
What I’m doing: I’m currently building element components for more reusability and accessibility. If you’re interested, then please sign up for my newsletter here to be the first to receive a Github link(it’s free).