How to make a reusable button component with Typescript in React applications
mihomihouk
Posted on July 21, 2023
Introduction
Do you find your reusable component getting messier as your project develops? If so, it might be a good time to start refactoring it!
In this article, I will share one way to quickly improve the readability and maintainability of a reusable component using a button component as an example.
Cluttered button component
Here we have a reusable, but very cluttered button component.
import { Link, LinkProps } from 'react-router-dom';
import React, { CSSProperties } from 'react';
import classNames from 'classnames';
import { FadeLoader } from 'react-spinners';
const spinnerOverride: CSSProperties = {
margin: '0 auto',
top: '30px'
};
interface ButtonProps {
className?: string;
onClick?: () => void;
to?: LinkProps['to'];
children?: React.ReactNode;
inNav?: boolean;
isPrimary?: boolean;
isSecondary?: boolean;
isWarning?: boolean;
disabled?: boolean;
isLoading?: boolean;
type: 'button' | 'reset' | 'submit' | undefined;
testId?: string;
}
export const Button: React.FC<ButtonProps> = ({
className,
onClick,
to,
inNav,
isPrimary,
isSecondary,
isWarning,
type,
testId,
disabled,
isLoading,
children
}) => {
const hrefTo = to ?? '#';
if (isLoading) {
return (
<FadeLoader
data-testid="loading"
loading={isLoading}
height={10}
width={10}
cssOverride={spinnerOverride}
aria-label="Loading Spinner"
/>
);
}
if (to) {
return (
<Link
className={classNames(
{
'pl-0 lg:pl-3 py-2 rounded-3xl hover:bg-gray-300 ease-in duration-300':
inNav
},
{
'w-full bg-primary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-primary-800':
isPrimary
},
{
'w-full bg-secondary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-secondary-800':
isSecondary
},
{
'w-full bg-error-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-error-800':
isWarning
},
{
'cursor-not-allowed': disabled
},
className
)}
to={hrefTo}
>
<button
className={classNames(
'w-full',
{ 'flex gap-4': inNav },
{
'cursor-not-allowed': disabled
}
)}
data-testid={testId}
tabIndex={0}
onClick={onClick}
disabled={disabled}
type={type}
>
{children}
</button>
</Link>
);
}
return (
<button
className={classNames(
{
'w-full bg-primary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
isPrimary
},
{
'w-full bg-secondary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
isSecondary
},
{
'w-full bg-error-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
isWarning
},
{
'py-2 lg:pl-3 rounded-3xl hover:bg-gray-300 ease-in duration-300 flex gap-4 w-full ':
inNav
},
{
'cursor-not-allowed': disabled
},
className
)}
data-testid={testId}
tabIndex={0}
onClick={onClick}
disabled={disabled}
type={type}
>
{children}
</button>
);
};
As a project develops, it's common for a reusable component to start accepting more and more props to extend its versatility. Depending on where it's used, we might want to pass different styles, types, IDs for testing, accessibility attributes, and more to a component.
But is it really necessary to explicitly write every single prop when we type, destructure, and consume them like this?
Absolutely not!
Refactor a reusable component using JSX.IntrinsicElements
One way to improve the readability and maintainability of a reusable component is to leverage React types.
Let's perform some magic!
import { Link, LinkProps } from 'react-router-dom';
import React, { CSSProperties } from 'react';
import classNames from 'classnames';
import { FadeLoader } from 'react-spinners';
const spinnerOverride: CSSProperties = {
margin: '0 auto',
top: '30px'
};
type IntrinsicButtonProps = JSX.IntrinsicElements['button'];
interface ButtonProps extends IntrinsicButtonProps {
to?: LinkProps['to'];
inNav?: boolean;
isPrimary?: boolean;
isSecondary?: boolean;
isWarning?: boolean;
isLoading?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
className,
to,
inNav,
isPrimary,
isSecondary,
isWarning,
disabled,
isLoading,
children,
...props
}) => {
const hrefTo = to ?? '#';
if (isLoading) {
return (
<FadeLoader
data-testid="loading"
loading={isLoading}
height={10}
width={10}
cssOverride={spinnerOverride}
aria-label="Loading Spinner"
/>
);
}
if (to) {
return (
<Link
className={classNames(
{
'pl-0 lg:pl-3 py-2 rounded-3xl hover:bg-gray-300 ease-in duration-300':
inNav
},
{
'w-full bg-primary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-primary-800':
isPrimary
},
{
'w-full bg-secondary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-secondary-800':
isSecondary
},
{
'w-full bg-error-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-error-800':
isWarning
},
{
'cursor-not-allowed': disabled
},
className
)}
to={hrefTo}
>
<button
className={classNames(
'w-full',
{ 'flex gap-4': inNav },
{
'cursor-not-allowed': disabled
}
)}
{...props}
>
{children}
</button>
</Link>
);
}
return (
<button
className={classNames(
{
'w-full bg-primary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
isPrimary
},
{
'w-full bg-secondary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
isSecondary
},
{
'w-full bg-error-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
isWarning
},
{
'py-2 lg:pl-3 rounded-3xl hover:bg-gray-300 ease-in duration-300 flex gap-4 w-full ':
inNav
},
{
'cursor-not-allowed': disabled
},
className
)}
{...props}
>
{children}
</button>
);
};
Wow!
Can you see the difference from the previous code?
The amount of code clearly reduced, making the component a lot easier to read.
So what have been changed?
Change the type of the props
The biggest difference is how we declare the type of the props.
type IntrinsicButtonProps = JSX.IntrinsicElements['button'];
First, this line of code extends JSX.IntrinsicElements['button']
, which includes all the standard attributes available for a element.
interface ButtonProps extends IntrinsicButtonProps {
to?: LinkProps['to'];
inNav?: boolean;
isPrimary?: boolean;
isSecondary?: boolean;
isWarning?: boolean;
isLoading?: boolean;
}
And then, we declare a new interface ButtonProps
by adding custom attributes to IntrinsicButtonProps
, which already has the basic button attributes.
Adjust the way to destructure and use props
The second difference is the way we destructure and use the props.
export const Button: React.FC<ButtonProps> = ({
className,
to,
inNav,
isPrimary,
isSecondary,
isWarning,
disabled,
isLoading,
children,
...props
}) => {
<button
className={classNames(
'w-full',
{ 'flex gap-4': inNav },
{
'cursor-not-allowed': disabled
}
)}
{...props}
>
{children}
</button>
When destructing, we extract some items by explicitly writing each one of them while handling the rest by using spread operator.
The extraction is useful for an item used conditionally as you can see in my example. We pass the rest to button
tag by again using spread operator.
That's it!
Final remark
I believe that enhancing the versatility of a reusable component necessities some refactoring.
Using the JSX.IntrinsicElements
is one of the approach and we already see a huge improvement in code.
What is your strategy to extend the versatility of a reusable component without compromising the readability and maintainability?
I am keen to know!
Posted on July 21, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.