React CDN (importmap版)の紹介

# はじめに

HTMLファイル1つで動作する、Reactのシンプルなサンプルコードを紹介します。
`importmap`を活用した例はまだ多くないため、参考になるかと思い公開しました。

本格的な開発には向きませんが、開発環境やサーバを用意せず、ローカルでHTMLファイルを開くだけで動作します。
開発者でない方に簡単な動作サンプルを見せる場合などに有用です。

# 1. React + MUI

MUI を使った基本のサンプルです。
ESModulesと、ReactのJSXを利用するためにBabelを利用した簡易デモとなっています。
HTMLファイルとして保存してブラウザで開くだけで動作します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4073727/b31b924e-57d6-431e-a447-cd791f05991a.png" width=50% />

```html:react_mui.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>ReactCDN版</title>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1, width=device-width">
  <script type="importmap">
  {
    "imports": {
      "react": "https://esm.sh/react@18",
      "react-dom/client": "https://esm.sh/react-dom@18/client",
      "react/jsx-runtime": "https://esm.sh/react@18/jsx-runtime",
      "@mui/material": "https://esm.sh/@mui/material@7?external=react"
    }
  }
  </script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js" crossorigin="anonymous"></script>
</head>
<body>
  <div id="app"></div>
  <script type="text/babel" data-type="module">
    import React, { useState, useEffect } from "react"
    import ReactDOM from "react-dom/client"
    import { Container, Card, CardHeader, CardContent, Button } from "@mui/material"

    const Counter = () => {
      const [count, setCount] = useState(0)

      const handleClick = () => {
        setCount((n) => n + 1)
      }

      return (
        <Container maxWidth="sm" sx={{ pt: 5 }}>
          <Card>
            <CardHeader title="Counter" />
            <CardContent>
              <Button variant="contained" onClick={handleClick}>Count: {count}</Button>
            </CardContent>
          </Card>
        </Container>
      )
    }

    import { createTheme, ThemeProvider, CssBaseline } from "@mui/material"

    const App = () => {
      const theme = createTheme({
        palette: {
          primary: { light: '#63ccff', main: '#009be5', dark: '#006db3' },
          background: { default: '#f5f5f5' }
        }
      })
      return (
        <ThemeProvider theme={theme}>
          <CssBaseline />
          <Counter />
        </ThemeProvider>
      )
    }

    const container = document.getElementById("app")
    const root = ReactDOM.createRoot(container)
    root.render(<App />)
  </script>
</body>
</html>
```

# 2. 地図のサンプル

地図には、LeafletとOpenStreetMapを利用しています。
こちらもHTMLファイルとして保存し、ブラウザで開くだけで動作します。

クリックするとマーカーを設置するサンプルとなっています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4073727/b9950b3f-cf90-4332-9a84-39ea9f3edb2b.png" width=50% >

```html:react_leaflet.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>ReactCDN版</title>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1, width=device-width">
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400;500;600;700&display=swap">
  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
  <style>
    #map {
      height: 600px;
    }
  </style>
  <script type="importmap">
  {
    "imports": {
      "react": "https://esm.sh/react@18",
      "react-dom/client": "https://esm.sh/react-dom@18/client",
      "react/jsx-runtime": "https://esm.sh/react@18/jsx-runtime",
      "@mui/material": "https://esm.sh/@mui/material@7?external=react",
      "leaflet": "https://esm.sh/leaflet@1.9.4"
    }
  }
  </script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js" crossorigin="anonymous"></script>
</head>
<body>
  <div id="app"></div>
  <script type="text/babel" data-type="module">
    import React, { useState, useEffect } from "react"
    import { Container, Card, CardHeader, CardContent, Button, Icon } from "@mui/material"
    import L from "leaflet"

    const Map = (props) => {
      const [markers, setMarkers] = useState([])

      useEffect(() => {
        const map = L.map('map').setView([35.6, 140], 10)
        const tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
          { attribution: "<a href='http://osm.org/copyright'> ©OpenStreetMap </a>" }
        ).addTo(map)
        tileLayer.addTo(map)

        map.on('click', function (e) {
          const marker = L.marker([e.latlng.lat, e.latlng.lng]).addTo(map).bindPopup('<b>Name</b><br />Comment.').openPopup()
          setMarkers([...markers, { marker: marker, lat: e.latlng.lat, lng: e.latlng.lng }])
        })
      }, [])

      return (
        <Card sx={{ width: "800px", m: "50px auto" }}>
          <CardHeader title="Map" />
          <CardContent>
            <div id="map"></div>
          </CardContent>
        </Card>
      )
    }

    import ReactDOM from "react-dom/client"
    import { createTheme, ThemeProvider, CssBaseline } from "@mui/material"

    const App = () => {
      const theme = createTheme({
        typography: {
          fontFamily: 'Noto Sans JP, sans-serif',
        },
        palette: {
          primary: { light: '#63ccff', main: '#009be5', dark: '#006db3' },
          background: { default: '#f5f5f5' }
        }
      })
      return (
        <ThemeProvider theme={theme}>
          <CssBaseline />
          <Map />
        </ThemeProvider>
      )
    }

    const container = document.getElementById("app")
    const root = ReactDOM.createRoot(container)
    root.render(<App />)
  </script>
</body>
</html>
```

# まとめ

このように、HTMLファイルだけでもReactの機能を活用することができます。

シンプルな構成でReact学習の最初の一歩にも悪くないとは思いますが、この方法は開発環境やTypeScriptのサポートが不足するため、簡単な確認をしたあとは一般的な開発環境を整えることをお勧めします。

なお、他にもReact Hook Formsを使ったフォームバリデーションや、Amazon Cognito + TanStack Queryあたりを使った認証付きページのフロントエンドもHTMLファイル1つで実装可能です。Vue3でも同様のことができます。HTMLだけでこれらの機能を動かすのもこれで面白いとは思っています。