I have a Svg component like so
interface SvgIconProps {
children: React.ReactNode;
strokeWidth?: number;
width?: number;
height?: number;
className?: string;
}
export const SvgIcon = ({
children,
strokeWidth = 1,
width = 24,
height = 24,
className,
}: SvgIconProps) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={className}
fill="none"
viewBox={`0 0 ${width} ${height}`}
stroke="currentColor"
strokeWidth={strokeWidth}
width={width}
height={height}
>
{children}
</svg>
);
};
And this component accepts children, and everything was ok but now I want to make a ButtonWithIcon component and I want this component to accept children and the SVG markup, but I want to make sure that nothing that is not svg markup would not slip in, it seems that the SVG markup is typed as ReactSVG and when I change the type of children in SvgIcon component to be ReactSvg I get error from typescript
Type 'ReactSVG' is not assignable to type 'ReactNode'.ts(2322)
and actually type SvgFactory is
interface SVGFactory extends DOMFactory<SVGAttributes<SVGElement>, SVGElement> {
(props?: ClassAttributes<SVGElement> & SVGAttributes<SVGElement> | null, ...children: ReactNode[]): ReactSVGElement;
}
so its expecting ReactNode[], so is there any way of differentiating children React.ReactNode for the button from React.ReactNode for SvgIcon component, or maybe to hinder SvgIcon component to only accept svg specific markup?
this is my button so far
import { SvgIcon } from "../SvgIcon";
interface ButtonWithIconProps {
children: React.ReactNode;
onClick: (...args: any) => void;
side: "left" | "right";
svgMarkup: React.ReactNode;
}
export const ButtonWithIcon = ({
children,
onClick,
side,
svgMarkup,
}: ButtonWithIconProps) => {
return (
<button onClick={onClick} className="">
{side === "left" && <SvgIcon>{svgMarkup}</SvgIcon>}
{children}
{side === "right" && <SvgIcon>{svgMarkup}</SvgIcon>}
</button>
);
};
or would that be better practice to just use the Image element? I don't want to have plenty of svg images in my public folder so I would prefer to use markup.
Thanks
CodePudding user response:
You don't have to place SVGs in the public
folder - they are components and can be stored as such.
Typically, every SVG
is its own file. You also don't need to set default props
via destructing; they are overridden if the props
object is spread after the default attributes.
import { FC, SVGProps } from 'react';
export const ExampleIcon: FC<SVGProps<SVGSVGElement>> = (props) => (
<svg
aria-label="Example icon"
className="h-6"
fill="none"
height={24}
role="presentation"
stroke="currentColor"
strokeWidth={1}
viewBox="0 0 24 24"
width={24}
{...props}
>
{/* complete svg contents not children */}
<path d="" />
</svg>
);
Example usage
import { ButtonHTMLAttributes, FC } from "react";
interface ButtonWithIconProps {
children: React.ReactNode;
leftIcon: React.ReactNode;
rightIcon: React.ReactNode;
}
export const ButtonWithIcon:FC<ButtonWithIconProps & ButtonHTMLAttributes<HTMLButtonElement>> = ({ children, leftIcon, rightIcon, ...rest }) => (
<button {...rest}>
{leftIcon}
{children}
{rightIcon}
</button>
);
// with props
<ButtonWithIcon leftIcon={<ExampleIcon className="text-red" width={30}/>} />
// without props
<ButtonWithIcon leftIcon={ExampleIcon} />
If you pass the SVG
content as children
, you create more code than just keeping one icon per file. Also, when you change the icon, you only have to change it in one place.
React has a default button type React.ButtonHTMLAttributes<HTMLButtonElement>
. It already includes onClick
and other default button HTML
attributes.
It's up to you to use images, SVGs, or both.
SVGs are sent as HTML
and do not require any extra server fetching. However, big SVGs can add a lot of HTML
that is rendered with the original content. SVGs scale to any size without needing anything extra.
Images make an additional server request. However, you can defer them from loading until they are needed. Most sites use multiple image sizes for the same images to deliver an image that matches the user's device without the additional size.