Best practices for typing a reusable button?

I have a React-TypeScript-Material UI app with a Button component but I'm not sure if what I'm doing is a good practice. Basically in the ButtonProps 4 out of 5 props are optional. Is this ok or should I structure the code in a different way? Perhaps not turning the button into a reusable component at all?
// src\components\shared\Button.tsx
/** @jsxImportSource @emotion/react */
import React, { ReactElement } from 'react';
import { css } from '@emotion/react';

type ButtonProps = {
children: string;
leftIcon?: ReactElement<SVGSVGElement>;
onClick?: () => void;
disabled?: boolean;
'data-testid'?: string;
};

const styles = (leftIcon: ReactElement<SVGSVGElement> | undefined) => css`
position: relative;
border-radius: 8px;
border: 2px solid transparent;
padding: ${leftIcon ? '0.6em 1em 0.6em 0.8em' : '0.5em 1.2em 0.6em 1.2em'};
`;

const iconContainerStyle = css`
// more styling
`;

export const Button = ({
children,
leftIcon,
onClick,
disabled,
'data-testid': testId
}: ButtonProps) => {
return (
<button
css={styles(leftIcon)}
onClick={onClick}
disabled={disabled}
data-testid={testId}
>
{leftIcon && <span css={iconContainerStyle}>{leftIcon}</span>}
{children}
</button>
);
};
// src\components\shared\Button.tsx
/** @jsxImportSource @emotion/react */
import React, { ReactElement } from 'react';
import { css } from '@emotion/react';

type ButtonProps = {
children: string;
leftIcon?: ReactElement<SVGSVGElement>;
onClick?: () => void;
disabled?: boolean;
'data-testid'?: string;
};

const styles = (leftIcon: ReactElement<SVGSVGElement> | undefined) => css`
position: relative;
border-radius: 8px;
border: 2px solid transparent;
padding: ${leftIcon ? '0.6em 1em 0.6em 0.8em' : '0.5em 1.2em 0.6em 1.2em'};
`;

const iconContainerStyle = css`
// more styling
`;

export const Button = ({
children,
leftIcon,
onClick,
disabled,
'data-testid': testId
}: ButtonProps) => {
return (
<button
css={styles(leftIcon)}
onClick={onClick}
disabled={disabled}
data-testid={testId}
>
{leftIcon && <span css={iconContainerStyle}>{leftIcon}</span>}
{children}
</button>
);
};
Solution:
You can have something like this ```ts type ButtonProps = React.ComponentProps<"button"> & { leftIcon?: ReactElement<SVGSVGElement>;...
Jump to solution
3 Replies
Henil Shah
Henil Shah2mo ago
I think with components arch, the best part is you decide what and how much connection you want. IMO, if you have 3 / 4 button types (Default, Outline, Success, Danger, etc) then this code makes sense. But if you almost have the same button types across your app then this can be avoided.
Solution
Matvey
Matvey2mo ago
You can have something like this
type ButtonProps = React.ComponentProps<"button"> & {
leftIcon?: ReactElement<SVGSVGElement>;
'data-testid'?: string;
};
type ButtonProps = React.ComponentProps<"button"> & {
leftIcon?: ReactElement<SVGSVGElement>;
'data-testid'?: string;
};
and then
<button
css={styles(leftIcon)}
data-testid={testId}
{...otherProps}
>
<button
css={styles(leftIcon)}
data-testid={testId}
{...otherProps}
>
Juraj98
Juraj982mo ago
+1 for @Matvey's solution of extending ComponentProps. I'd also like to argue, that generally, have as much of the props optional as possible. If I take component made by someone else, most of the time the first thing I want to do is render it, to see how it behaves "by default" and what happens when I start to add additional props. In this case it doesn't matter that much, because everyone generally understands what children do, but I'd still have it optional as a rule of thumb. And of course, this is my personal preference, do it or don't based on how it feels to you. Another thing I'd change is leftIcon to ReactNode, instead of ReactElement<SVGSVGElement>. That way you can pass something that's not a svg. (For example loading spinner component).