FlowingDev

ReactのSVG:静的イメージから動的コンポーネントへ

静的なSVG画像を、propsを受け取って色などを変更できる、動的で再利用可能なReactコンポーネント(JSX/TSX)に変換する方法を学びます。

ツールを試す: SVGからJSX

一言で言うと

SVGからJSXへの変換は、ベクター画像のコードを再利用可能なReactコンポーネントに変身させます。これにより、単なる静的なファイルではなく、propsで動的に制御できるようになります。

これが解決する問題

Webの古き良き時代(まあ、5年くらい前の話ですが)にSVGアイコンを表示したかったら、たぶん<img>タグに放り込むだけだったでしょう: <img src="/icons/user.svg" alt="User profile" />。はい、おしまい。シップイット!

でも、このシンプルなアプローチはちょっとした罠です。あなたのSVGは今やブラックボックス。ボタンにホバーしたときに色を変えたい?残念でした。ブラウザはSVGを他の画像ファイル(JPGとかPNGとか)と同じように扱うので、CSSでその中身を簡単にスタイリングすることはできません。アイコン内のパスの1つをアニメーションさせたい?無理ですね。ダークモードのテーマに合わせて色を切り替えたい?となると、全く新しいファイル(user-white.svg)を作って、srcを切り替えるロジックを書く羽目になります。あっという間にゴチャゴチャしてきます。

「あっ!」と賢い開発者は言います。「SVGってただのXMLマークアップじゃん!.svgファイルからコードをコピーして、コンポーネントのreturn文に直接貼り付けちゃえ!」

// MyComponent.js
const MyComponent = () => (
  <div>
    <p>My Profile</p>
    {/* I just pasted this from a .svg file! */}
    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M12 12C14.21 12 16 10.21 16 8C16 5.79 14.21 4 12 4C9.79 4 8 5.79 8 8C8 10.21 9.79 12 12 12ZM12 14C9.33 14 4 15.34 4 18V20H20V18C20 15.34 14.67 14 12 14Z" fill="black"/>
    </svg>
  </div>
);

これは「インライン化」と呼ばれ、これだけでも大きな進歩です。これでSVGはDOMの一部となり、CSSでスタイリングできるようになります。しかし、一つの問題を解決した代わりに、別の問題を生み出してしまいました。

  1. ごちゃごちゃしてる: SVGコードの塊がコンポーネントを読みにくくします。もしそこにアイコンが5つあったらどうなるか、想像してみてください。
  2. 再利用できない: そのユーザーアイコンが他の場所でも必要になった?あのコードの塊をまた丸ごとコピペする準備をどうぞ。
  3. JSXじゃない: Reactには独自のHTML方言があります。stroke-widthのような属性はstrokeWidthのようにcamelCaseにする必要があり、classはclassNameになります。これらの変換を忘れると、何も言わずに壊れるか、コンソールにやかましい警告が出ることになります。

本当の解決策は、アイコンを静的なアセットとしてではなく、コンポーネントエコシステムの第一級市民(first-class citizen)として扱うことです。やりたいのは<UserIcon color="blue" size={24} />のようなことでしょう。これこそが、SVGからJSXへの変換が解決する問題です。生のSVGマークアップを、クリーンで再利用可能で、props駆動のReactコンポーネントに変えるという定型作業を自動化してくれるのです。

内部の仕組み

ただのテキストの塊が、洗練されたReactコンポーネントに変わるのは魔法のように見えるかもしれませんが、これは予測可能なステップバイステップのプロセスです。では、その舞台裏を覗いてみましょう。

ステップ1:SVGマークアップのパース

まず、ツールはSVGを理解しなければなりません。SVGはXMLの方言なので、HTMLドキュメントと同じように、ノードのツリー構造にパース(解析)できます。各要素(<svg>、<path>、<g>など)がノードになり、その属性(width、fill、dなど)がノードのプロパティになります。

つまり、これは:

<svg width="24" height="24">
  <path fill-rule="evenodd" d="..." />
</svg>

パーサーには次のようなツリーとして理解されます:

  • svgノード、属性はwidth="24"とheight="24"
    • pathノード、属性はfill-rule="evenodd"とd="..."

ステップ2:属性のJSX化

ここでReact特有の変換が行われます。パーサーはノードツリーをたどり、属性をJSXに準拠するように変換します。

  • キャメルケース化: ハイフン(kebab-case)を持つ属性はcamelCaseに変換されます。fill-ruleはfillRuleに、stroke-widthはstrokeWidthになります。
  • キーワードのマッピング: JavaScriptの予約語は置き換えられます。最も一般的なのはclassで、これはclassNameになります。
  • style属性: style="fill:red; stroke:blue"のような属性は、style={{ fill: 'red', stroke: 'blue' }}というJavaScriptオブジェクトに変換されます。

というわけで、<path fill-rule="evenodd" class="icon-path">は、JSXの<path fillRule="evenodd" className="icon-path">に変換されます。

ステップ3:コンポーネントでラップする

SVGツリーが流暢なJSXを話せるようになったところで、次のステップは、それを標準的なReactの関数コンポーネントの構造でラップすることです。最もシンプルなバージョンはこんな感じです:

import React from 'react';

const MyIcon = () => {
  return (
    <svg width="24" height="24" viewBox="0 0 24 24">
      {/* ... all the JSX-ified paths and other elements ... */}
    </svg>
  );
};

export default MyIcon;

これだけでも再利用可能な<MyIcon />コンポーネントが手に入り、大きな勝利です。しかし、本当の威力は、これを動的にすることにあります。

ステップ4:propsとcurrentColorの追加

ここが秘伝のタレです。良いコンバーターは、SVGの元の属性をハードコーディングするだけではありません。それらを設定可能にしてくれます。

  • propsの展開: コンポーネントがpropsを受け入れ、ルートの<svg>要素にそれら(props)を展開するように変更されます。これはよく...rest構文を使って行われます。これで、標準的なSVG属性(width、height、className、aria-labelなど)をコンポーネントに直接渡せるようになります。

  • currentColorのトリック: これはスタイリングにおけるゲームチェンジャーです。コンバーターは、アイコンの色を定義しているハードコードされたfillやstroke属性(例: fill="#000000")を見つけ出し、それを特別なCSSキーワードであるcurrentColorに置き換えます。

currentColorはブラウザに「この要素のCSSのcolorプロパティが何であれ、それをfill/strokeに使え」と伝えます。つまり、アイコンがテキストのように振る舞うようになるのです。アイコンをdivで囲んでcolor: blueと設定すれば、アイコンは青くなります。

// The upgraded component
const MyIcon = ({ color = 'currentColor', size = 24, ...rest }) => {
  return (
    <svg 
      width={size} 
      height={size} 
      fill={color} // or applied directly to paths
      {...rest}
    >
      <path d="..." fill="currentColor" />
    </svg>
  );
};

ステップ5:refのフォワーディング

親コンポーネントから実際の<svg> DOM要素への直接のハンドル(参照)が必要になることがあります。位置を測定したり、サードパーティのライブラリで使ったりするためです。通常の関数コンポーネントは、その内部のDOM構造を隠蔽します。その解決策がReact.forwardRefです。

洗練されたコンバーターは、コンポーネントをReact.forwardRefでラップします。これにより、親から渡されたrefを、その下にある<svg>タグまで直接通すためのパイプが作られます。

最終的な、本番環境レベルのコンポーネントは、だいたいこんな感じになります(TypeScriptの例):

import * as React from 'react';

type Props = React.SVGProps<SVGSVGElement>;

const MyIcon = React.forwardRef<SVGSVGElement, Props>((props, ref) => (
  <svg ref={ref} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
    <path d="..." fill="currentColor" />
  </svg>
));

MyIcon.displayName = 'MyIcon'; // Good for debugging
export default MyIcon;

実世界のストーリー

テーマ適用の悪夢

あるフロントエンドチームが、ライトモードとダークモードの両方を備えた美しいWebアプリを構築していました。彼らは約50個のアイコンを使っており、すべて<img src="/icons/whatever.svg">として実装されていました。アイコンは黒色です。ダークモードをリリースしたとき、パニックが起こりました。新しい暗い背景に対して、50個すべての黒いアイコンが消えてしまったのです。彼らが最初にあわてて行った「修正」は、白いアイコンのセカンドセット(whatever-dark.svg)をエクスポートし、至る所にisDarkMode ? 'whatever-dark.svg' : 'whatever.svg'のような厄介な条件ロジックを書くことでした。これはメンテナンスの悪夢でした。

その後、ある開発者がSVGからJSXへのワークフローを導入しました。彼はスクリプトを実行して50個すべてのSVGをReactコンポーネントに変換し、決定的なこととして、すべてのfill="black"属性をfill="currentColor"に置き換えました。彼らはすべての<img>タグを、<WhateverIcon />のような新しいアイコンコンポーネントに置き換えました。すると、アイコンは即座に親のテキストカラーを継承するようになり、それはすでにテーマのCSSで処理されていました。ライトモード:黒いテキスト、黒いアイコン。ダークモード:白いテキスト、白いアイコン。ただ、うまくいったのです。

教訓: SVGをコンポーネントとしてインライン化することで、アプリの既存のデザインシステムやスタイリングとシームレスに統合でき、アセット管理地獄からあなたを救い出します。

アニメーションするチェックマーク

あるプロダクトマネージャーが「心地よい」アニメーションを望んでいました。ユーザーがフォームを完了すると、チェックマークが画面上で動的に描画されるというものです。アニメーションGIFを使うのは論外でした。ピクセルが荒く見え、ファイルサイズも大きかったからです。開発者はライブラリを探してみましたが、カスタムデザインのチェックマークにぴったり合うものは何もありませんでした。

そこで彼女は自ら問題解決に乗り出し、デザイナーからもらった静的なチェックマークのSVGをJSXコンポーネントに変換しました。これにより、Reactコード内でチェックマークの<path>要素に直接アクセスできるようになったのです。そのパスのstroke-dasharrayとstroke-dashoffsetプロパティに単純なCSSトランジションを使うことで、彼女が望んでいた通りの「描画」エフェクトを作り出すことができました。アニメーションはバターのように滑らかで、無限に拡大縮小可能で、コードは1KB未満でした。

教訓: SVGをコンポーネントに変換すると、その個々のパーツに対するきめ細かな制御がアンロックされ、複雑なCSSやJavaScript駆動のアニメーションが可能になります。

よくある間違いと落とし穴

  • アクセシビリティを忘れる。 意味を伝えるアイコン(「閉じる」のXなど)は、単なる装飾ではありません。変換時に<title>タグを取り除いてしまうと、スクリーンリーダーはそれを「グループ」と読み上げたり、単に無視したりするかもしれません。最終的なコンポーネントが適切なaria-labelプロップを持つか、SVG内の<title>要素を介してアクセシブルな名前を持つように常に確認してください。良いコンポーネントは<svg role="img" ...>のようにレンダリングされます。
  • バンドルを肥大化させる。 SVGをインライン化すると、そのコードはJavaScriptバンドルの一部になります。数十個の小さなUIアイコンであれば、これは(HTTPリクエストが減るため)純粋な利益です。しかし、何千ものパスを持つ巨大で複雑なイラストをインライン化しようとすると、バンドルサイズが深刻に肥大化し、初期ページの読み込みが遅くなる可能性があります。大きくて静的なイラストには、今でも<img>が全くもって有効な選択肢です。
  • まず最適化を無視する。 FigmaやAdobe IllustratorのようなデザインツールからエクスポートされたSVGは、散らかっていることで有名です。エディターのメタデータ、コメント、非表示レイヤー、過度に正確な小数点以下の値などが含まれています。これを直接JSXコンポーネントに突っ込むと、肥大化して読みにくいコードになります。変換する前に、必ずSVGOのようなオプティマイザでSVGを実行してください。これにより、コードサイズを50%以上削減できることがよくあります。
  • 手動編集が同期の問題になる。 icon.svgをIcon.jsxに変換したとします。その後、Icon.jsxを手動でいじって機能を追加します。1週間後、デザイナーが更新されたicon.svgを渡してきました。さあ、どうしますか?再変換を実行して手動編集を失うか、編集したJSXに新しいデザインを手動でパッチしようと試みるか。信頼できる情報源(source of truth)は.svgファイルであるべきです。JSXへの変換は、再現可能で自動化されたスクリプトであるべきです。

なぜこれを気にかけておくべきか

コンポーネントベースのWebアプリケーション(React、Preactなど)を構築していて、アイコンが単なる静的な画像以上のものである必要がある場合は、いつでもSVGからJSXへの変換を検討すべきです。

次のような場合に、これはうってつけのツールです:

  • 状態やpropsに応じてアイコンの色、サイズ、線の太さを変更する必要がある場合。
  • デザインシステムやコンポーネントライブラリを構築しており、アイコンをスケーラブルで一貫した方法で管理する必要がある場合。
  • アイコンの一部をアニメーションさせたい場合。
  • パフォーマンスを最適化しており、重要なアイコンをインライン化してネットワークリクエストの数を減らしたい場合。

現代のフロントエンド開発において、アイコンをコンポーネントとして扱うことはハックではなく、動的で保守性が高く、高性能なユーザーインターフェースを構築するための標準的な手法です。

もっと深く知る

  • MDN: SVG: Scalable Vector Graphics — ブラウザでSVGがどのように機能するかの究極ガイド。
  • W3C: Scalable Vector Graphics (SVG) 2 Specification — 公式仕様。内容は濃いですが、究極の信頼できる情報源です。
  • React Docs: Forwarding Refs — コンポーネント合成に不可欠なこのテクニックに関する公式ドキュメント。
  • MDN: currentColor — SVGのテーマ設定をいとも簡単にする、シンプルかつ強力なCSSの値。
  • CSS-Tricks: Styling SVG — CSSでSVGを操作し、スタイリングする多くの方法に関する素晴らしい記事のコレクション。
  • SVGO (SVG Optimizer) — SVGファイルをクリーンアップするための業界標準ツール。これが何をするかを理解すると、クリーンなSVGコンポーネントに何が必要かがわかるようになります。

理論はOK。さあ手を動かそう — 100%ブラウザ内で。

ツールを試す: SVGからJSX