🕌

手動でSVGファイルをReactコンポーネント化

2023/05/16に公開

TL;DR

  • SVG ファイルを手動で React コンポーネントに変換します。
  • stroke-widthfill-opacity など、必要に応じて適切な形に変換しないとエラーが発生します。
    • stroke-widthstrokeWidth
    • fill-opacityfillOpacity

はじめに

SVG ファイルを React コンポーネントに手動で変換する方法を記載します。

モチベーション

SVG ファイルを React コンポーネントとして利用したい。

SVGファイルをReactコンポーネントに変換する流れ

作業の流れ

  1. Next.js のプロジェクトを作成
  2. SVG ファイルを Web からダウンロード
  3. SVG ファイルの VSCode で開く
  4. SVG ファイルを React コンポーネントに変換
  5. インポートし画面に表示

メリット・デメリット

  • メリットは手作業だから作業が簡単
  • デメリットは更新がある際に都度処理が必要

では実際に SVG を React コンポーネントに変換します。

Next.jsのプロジェクトを作成

Next.js のプロジェクトを作成します。

$ pnpm create next-app next-svg-component --typescript --eslint --src-dir --import-alias "@/*" --use-pnpm --tailwind --app

実行ログ

Library/pnpm/store/v3/tmp/dlx-22604      | Progress: resolved 1, reused 0, dowLibrary/pnpm/store/v3/tmp/dlx-22604      |   +1 +
Library/pnpm/store/v3/tmp/dlx-22604      | Progress: resolved 1, reused 0, dowPackages are hard linked from the content-addressable store to the virtual store.
  Content-addressable store is at: /Users/hayato94087/Library/pnpm/store/v3
  Virtual store is at:             Library/pnpm/store/v3/tmp/dlx-22604/node_modules/.pnpm
Library/pnpm/store/v3/tmp/dlx-22604      | Progress: resolved 1, reused 0, dowLibrary/pnpm/store/v3/tmp/dlx-22604      | Progress: resolved 1, reused 0, downloaded 1, added 1, done
Creating a new Next.js app in /Users/hayato94087/Private/next-svg-component.

Using pnpm.

Initializing project with template: app-tw


Installing dependencies:
- react
- react-dom
- next
- typescript
- @types/react
- @types/node
- @types/react-dom
- tailwindcss
- postcss
- autoprefixer
- eslint
- eslint-config-next


   ╭─────────────────────────────────────────────────────────────────╮
   │                                                                 │
   │                Update available! 7.27.0 → 8.5.1.                │
   │   Changelog: https://github.com/pnpm/pnpm/releases/tag/v8.5.1   │
   │                Run "pnpm add -g pnpm" to update.                │
   │                                                                 │
   │     Follow @pnpmjs for updates: https://twitter.com/pnpmjs      │
   │                                                                 │
   ╰─────────────────────────────────────────────────────────────────╯

Downloading registry.npmjs.org/next/13.4.2: 12.3 MB/12.3 MB, done
Downloading registry.npmjs.org/@next/swc-darwin-x64/13.4.2: 34.3 MB/34.3 MB, donenloading registry.npmjs.org/@next/swc-darwin-x64/13.4.2: 34.3 MB/34.3 MB
Packages: +348
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Packages are hard linked from the content-addressable store to the virtual store.
  Content-addressable store is at: /Users/hayato94087/Library/pnpm/store/v3
  Virtual store is at:             node_modules/.pnpm
Progress: resolved 356, reused 331, downloaded 17, added 348, done

dependencies:
+ @types/node 20.1.4
+ @types/react 18.2.6
+ @types/react-dom 18.2.4
+ autoprefixer 10.4.14
+ eslint 8.40.0
+ eslint-config-next 13.4.2
+ next 13.4.2
+ postcss 8.4.23
+ react 18.2.0
+ react-dom 18.2.0
+ tailwindcss 3.3.2
+ typescript 5.0.4

The integrity of 5632 files was checked. This might have caused installation to take longer.
Done in 12.1s
Initialized a git repository.

Success! Created next-svg-component at /Users/hayato94087/Private/next-svg-component

cd next-svg-component

SVGファイルをWebからダウンロード

Web から SVG ファイルをダウンロードします。

https://commons.wikimedia.org/wiki/File:Scalable_Vector_Graphics_Fill-opacity.svg

SVGファイルのVSCodeで開く

ダウンロードしたファイルを VSCode で開きます。

Scalable_Vector_Graphics_Fill-opacity.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created by Erik Baas for WikiBooks -->
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
 <title>Wikibooks SVG-demo: fill-opacity</title>
 <rect id="background" width="100%" height="100%" fill="white" />
 <circle cx="40" cy="40" r="35" stroke="black" stroke-width="4" fill="lightgrey" />
 <circle cx="60" cy="60" r="35" stroke="black" stroke-width="4" fill="lightgrey" fill-opacity="0.5" />
</svg>

SVGファイルをReactコンポーネントに変換

TSX ファイルを作成します。

$ mkdir src/components
$ touch src/components/LogoSvg.tsx

SVG ファイルの中身をコピペします。props が渡せるように {...props} を追加しておきます。

src/components/LogoSvg.tsx
const LogoSvg = (
  props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
) => (
  <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" {...props}>
    <title>Wikibooks SVG-demo: fill-opacity</title>
    <rect id="background" width="100%" height="100%" fill="white" />
    <circle
      cx="40"
      cy="40"
      r="35"
      stroke="black"
      stroke-width="4"
      fill="lightgrey"
    />
    <circle
      cx="60"
      cy="60"
      r="35"
      stroke="black"
      stroke-width="4"
      fill="lightgrey"
      fill-opacity="0.5"
    />
  </svg>
);
export default LogoSvg;

インポートし画面に表示

画面に表示できるように、src/app/page.tsx を空にして、LogoSvg を追加します。

src/app/page.tsx
import LogoSvg from '@/components/LogoSvg'

export default function Home() {
  return (
    <div>
      <LogoSvg />
    </div>
  )
}

開発環境で起動します。

$ pnpm dev

実行ログ

> next-svg-component@0.1.0 dev /Users/hayato94087/Private/next-svg-component
> next dev

- warn Port 3000 is in use, trying 3001 instead.
- ready started server on 0.0.0.0:3001, url: http://localhost:3001
- event compiled client and server successfully in 2s (311 modules)
- wait compiling...
- wait compiling /page (client and server)...
- event compiled client and server successfully in 3.3s (586 modules)
- wait compiling...
- event compiled successfully in 120 ms (334 modules)
- wait compiling /favicon.ico/route (client and server)...
- event compiled client and server successfully in 439 ms (618 modules)
- wait compiling...

エラー対応

しばらくすると、以下のようにエラーがでます。

Warning: Invalid DOM property `stroke-width`. Did you mean `strokeWidth`?
    at circle
    at svg
    at div
    at InnerLayoutRouter (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/layout-router.js:224:11)
    at RedirectErrorBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/redirect-boundary.js:72:9)
    at RedirectBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/redirect-boundary.js:80:11)
    at NotFoundBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/not-found-boundary.js:40:11)
    at LoadingBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/layout-router.js:328:11)
    at ErrorBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:86:11)
    at InnerScrollAndFocusHandler (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/layout-router.js:139:9)
    at ScrollAndFocusHandler (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/layout-router.js:211:11)
    at RenderFromTemplateContext (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/render-from-template-context.js:15:44)
    at Lazy
    at OuterLayoutRouter (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/layout-router.js:337:11)
    at Lazy
    at body
    at html
    at RedirectErrorBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/redirect-boundary.js:72:9)
    at RedirectBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/redirect-boundary.js:80:11)
    at NotFoundErrorBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/not-found-boundary.js:33:9)
    at NotFoundBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/not-found-boundary.js:40:11)
    at ReactDevOverlay (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:66:9)
    at HotReload (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:277:11)
    at Router (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/app-router.js:86:11)
    at ErrorBoundaryHandler (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:62:9)
    at ErrorBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:86:11)
    at AppRouter (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/app-router.js:361:13)
    at Lazy
    at Lazy
    at ServerComponentWrapper (/Users/hayato94087/Private/next-svg-component/node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/server/app-render/create-server-components-renderer.js:78:31)
    at ServerComponentWrapper (/Users/hayato94087/Private/next-svg-component/node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/server/app-render/create-server-components-renderer.js:78:31)
    at InsertedHTML (/Users/hayato94087/Private/next-svg-component/node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/server/app-render/app-render.js:835:33)
Warning: Invalid DOM property `fill-opacity`. Did you mean `fillOpacity`?
    at circle
    at svg
    at div
    at InnerLayoutRouter (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/layout-router.js:224:11)
    at RedirectErrorBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/redirect-boundary.js:72:9)
    at RedirectBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/redirect-boundary.js:80:11)
    at NotFoundBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/not-found-boundary.js:40:11)
    at LoadingBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/layout-router.js:328:11)
    at ErrorBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:86:11)
    at InnerScrollAndFocusHandler (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/layout-router.js:139:9)
    at ScrollAndFocusHandler (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/layout-router.js:211:11)
    at RenderFromTemplateContext (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/render-from-template-context.js:15:44)
    at Lazy
    at OuterLayoutRouter (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/layout-router.js:337:11)
    at Lazy
    at body
    at html
    at RedirectErrorBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/redirect-boundary.js:72:9)
    at RedirectBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/redirect-boundary.js:80:11)
    at NotFoundErrorBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/not-found-boundary.js:33:9)
    at NotFoundBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/not-found-boundary.js:40:11)
    at ReactDevOverlay (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:66:9)
    at HotReload (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:277:11)
    at Router (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/app-router.js:86:11)
    at ErrorBoundaryHandler (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:62:9)
    at ErrorBoundary (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:86:11)
    at AppRouter (webpack-internal:///(sc_client)/./node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/app-router.js:361:13)
    at Lazy
    at Lazy
    at ServerComponentWrapper (/Users/hayato94087/Private/next-svg-component/node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/server/app-render/create-server-components-renderer.js:78:31)
    at ServerComponentWrapper (/Users/hayato94087/Private/next-svg-component/node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/server/app-render/create-server-components-renderer.js:78:31)
    at InsertedHTML (/Users/hayato94087/Private/next-svg-component/node_modules/.pnpm/next@13.4.2_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/server/app-render/app-render.js:835:33)

注目すべき点を抜粋すると以下です。

Warning: Invalid DOM property `stroke-width`. Did you mean `strokeWidth`?
Warning: Invalid DOM property `fill-opacity`. Did you mean `fillOpacity`?

fill-opacitystroke-widthfillOpacitystrokeWidth に修正します。

src/components/LogoSvg.tsx
const LogoSvg = (
  props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
) => (
  <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" {...props}>
    <title>Wikibooks SVG-demo: fill-opacity</title>
    <rect id="background" width="100%" height="100%" fill="white" />
    <circle
      cx="40"
      cy="40"
      r="35"
      stroke="black"
-      stroke-width="4"
+      strokeWidth="4"
      fill="lightgrey"
    />
    <circle
      cx="60"
      cy="60"
      r="35"
      stroke="black"
-      stroke-width="4"
+      strokeWidth="4"
      fill="lightgrey"
-      fill-opacity="0.5"
+      fillOpacity="0.5"
    />
  </svg>
);
export default LogoSvg;

これでエラーが消えます。

まとめ

SVG ファイルを手動で React コンポーネントに変換しました。stroke-widthfill-opacity など、必要に応じて適切な形に変換しないとエラーが発生しました。stroke-widthstrokeWidthfill-opacityfillOpacity を変換し修正しました。

Discussion