Reactで最低限のSVGアイコンコンポーネントを作る

2021-08-27

追記:2021/08/28 ボタンコンポーネントで、今回作成したアイコンを使用する際にbuttonタグの子要素にdivを使用するのはNGのためspanタグなどを設定してください!

SVGアイコンのコンポーネントを自作する必要があったのでそのメモです。

シンプルなSVGコンポーネント

以下の警告マークのSVGを単体で作る場合は、以下のようなコンポーネントになると思います。

警告マーク

import { VFC } from 'react';

interface Props {
  size?: number
  fill?: string
}

const AlertIcon: VFC<Props> = props => {
  const { size, fill } = props
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width={size}
      height={size}
      fill={fill}
      viewBox="0 0 16 16"><path d="M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"></path>
    </svg>
  );
};

export default AlertIcon;

使う側は以下のように使います。(本当はstrokeもpropsで渡せた方がいいかも。。)

import AlertIcon from '../AlertIcon'

<AlertIcon size={24} color="#000" />

シンプルなSVGコンポーネントの課題

これで基本OKかと思います。しかし数が多くなってきて、複数のSVGアイコンを1つのコンポーネントで、使いたいケースが出てきたときに、importが増えてちょっと微妙です。またよくあるのがボタンで左または、右などに矢印アイコンがついているようなボタンをよく見かけます。

別でボタンコンポーネントを作る際アイコンが、矢印だけであれば矢印アイコンだけをimportしてくれば良いですが

  • 矢印以外にも使いたい
  • アイコンなしのボタンもある

みたいな要件だった場合、少々アイコンとの組み合わせが悪くなります。今回はこういうケースを解決するための、最低限のSVGアイコンを作ってみたという記事です。

シンプルなSVGコンポーネントからPropsを削除

まずは、先程のシンプルSVGコンポーネントからPropsを削除しておきます。今回は.tsxファイル内にSVGのコードをラップしていますが、propsを渡さなくなったので、ただの.svgファイルでも問題ありません。

import { VFC } from 'react';

const AlertIcon: VFC = () => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 16 16"><path d="M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"></path>
    </svg>
  );
};

export default AlertIcon;

Icon.tsxを作成して全てのアイコンをimportしてtypeをもたせる

先程のAlertアイコンの他に、ArrowアイコンやUserアイコンなど複数のSVGがあると仮定して全てimportしてきます。またtypeというpropsを用意してアイコンの切り替えができるようにします。他にもアイコンをクリックしたらイベントが発火できるようにonClickも追加しておきます。(何故HTMLDivElementなのかは後でわかります。)

import AlertIcon from './Alert';
import ArrowIcon from './ArrowIcon';
import UserIcon from './UserIcon';

export const IconType = {
  AlertIcon,
  ArrowIcon,
  UserIcon
};

interface IconProps {
  size?: number;
  type: keyof typeof IconType;
  fill?: string;
  onClick?: React.MouseEventHandler<HTMLDivElement>;
}

Iconコンポーネントをラップする

まず先程設定したIconTypeを以下のようにコンポーネントとして使えるように設定します。この定数の部分がsvgタグになります。

const IconSvgFile = IconType[type];

JSXとスタイル部分を書いていきます。先程のIconSvgFileをdiv要素などでラップします。スタイリングもしつつラップした部分にpropsを設定します。 先に完成形を以下に貼ります。CSSは@emotion/styledを使っています。

import { VFC } from 'react';
import styled from '@emotion/styled';
import AlertIcon from './Alert';
import ArrowIcon from '.ArrowIcon';
import UserIcon from '.UserIcon';

export const IconType = {
  AlertIcon,
  ArrowIcon,
  UserIcon
};

interface IconProps {
  size?: number;
  type: keyof typeof IconType;
  fill?: string;
  onClick?: React.MouseEventHandler<HTMLDivElement>;
}

export const Icon: VFC<IconProps> = (props) => {
  const { size , type, onClick, fill } = props;
  const IconSvgFile = IconType[type];

  return (
    <IconWrapper size={size} onClick={onClick} fill={fill}>
      <IconSvgFile />
    </IconWrapper>
  );
};

const IconWrapper = styled.div<{ size: number; fill?: string;}>`
  width: ${({ size }) => size}px;
  height: ${({ size }) => size}px;
  cursor: ${({ onClick }) => (onClick ? 'pointer' : 'inherit')};
  fill: ${({ fill }) => (fill ? fill : '')};
`;

全てのpropsをdivに渡すことでSVGのサイズと色を制御しています。ちょっと荒業っぽいですがこれで使いやすい感じになりました。

<Icon type="FileIcon" size={24} fill="#f00" />

後でボタンコンポーネントを作る際に今回作ったtypeをそのまま渡せばアイコンありなしボタンが簡単に作成できそうです。

まとめ

  • SVGをdiv要素などでラップしたコンポーネントを作って全SVGアイコンをimportする
  • 複数コンポーネントをpropsで切り替えできるように設定する
  • アイコン付きボタンコンポーネントを作る際も同じpropsを渡せば簡単に実装できる

他にもいい方法があれば是非教えてください!!!!


ソースコードはこちらのリポジトリにあります。

Google Analyticsを使っています。