👶

息子専用アプリ開発①(React)

に公開

Dockerfile(開発用)

FROM node:20-alpine            # ベースに軽量な Node.js 公式イメージ(Alpine)を使用
WORKDIR /app                   # 以後の作業ディレクトリを /app に固定
EXPOSE 5173                    # Vite の開発サーバ既定ポート(5173)を宣言(実際の公開は compose 側で行う)
CMD ["npm", "run", "dev",      # コンテナ起動時に npm run dev を実行(dev サーバを立ち上げ)
     "--", "--host", "0.0.0.0",# 0.0.0.0 で待受 = Docker 外(ホスト)からアクセス&HMR可能に
     "--port", "5173"]         # ポートを 5173 に固定(compose の 5173:5173 と対応)

compose.yml(開発用)

name: ghost                         # プロジェクト名(コンテナ名に反映され、管理しやすい)
services:                           # ※複数形!最上位は services 固定
  web:                              # サービス名(内部 DNS 名にもなる)
    build:
      context: .                    # docker build の対象(カレントディレクトリ全体)
      dockerfile: Dockerfile        # 使う Dockerfile を明示
    ports:
      - "5173:5173"                 # ホスト 5173 → コンテナ 5173 を公開(ブラウザは http://localhost:5173)
    volumes:
      - .:/app                      # ホストの作業フォルダをコンテナ /app にマウント(保存→即HMR)
    environment:
      - CHOKIDAR_USEPOLLING=true    # ファイル監視をポーリングにして Docker/WSL でも安定させる
    command: npm run dev -- --host 0.0.0.0 --port 5173
                                    # build の CMD を上書き(内容は Dockerfile と同等。ここに集約でもOK)

vite.config.ts

import { defineConfig } from 'vite'   // Vite の型付き設定ヘルパーを読み込み
import react from '@vitejs/plugin-react' // React Fast Refresh 等を有効にする公式プラグイン

export default defineConfig({         // 設定オブジェクトをエクスポート(Vite が自動で読み取る)
  server: {
    host: true,                       // = 0.0.0.0 と同義。Docker 外からアクセス可能に
    port: 5173                        # 開発サーバのポート番号
  },
  plugins: [react()]                  // React 用プラグインを適用
})

package.json(抜粋・scripts)

{
  "scripts": {
    "dev": "vite",                    // 開発サーバ起動(HMR)
    "build": "vite build",            // 本番ビルド → dist/ に静的ファイル出力
    "preview": "vite preview --host 0.0.0.0 --port 4173"
                                      // 本番ビルド物をローカル配信して最終確認(Docker 外からも見える)
  }
}

src/main.tsx

import { StrictMode } from 'react'                      // 追加の警告やチェックを有効化(開発時の品質向上)
import { createRoot } from 'react-dom/client'           // React 18 以降のルート API
import App from './App'                                 // ルートコンポーネントを読み込み
import './App.css'                                      // 画面全体のスタイルを読み込み(順序が後ほどの上書きに影響)

createRoot(document.getElementById('root')!)            // index.html の #root に React アプリをマウント
  .render(
    <StrictMode>                                        {/* StrictMode 配下で App を描画(開発時のみ厳格チェック) */}
      <App />
    </StrictMode>
  )

補足:「!(ノンヌルアサーション)」は #root が必ず存在すると TypeScript に伝える記号です。


src/App.tsx(👻の表示とボタン挙動)

import { useState } from 'react'                         // React の状態管理フック
import ghostImg from './assets/ghost.png'                // 画像を import(ビルド時に最適化 & ハッシュ付与)

export default function App() {                          // App コンポーネント定義
  const [visible, setVisible] = useState(true)          // 👻 を表示中かどうかの状態(初期 true = 見えている)
  const [pressed, setPressed] = useState(false)         // ボタン“押下中”の一時的状態(見た目用)

  const toggle = () => setVisible(v => !v)              // 状態を反転する関数(true→false→true…)

  return (
    <div className="wrap">                               {/* 全体レイアウト用のラッパ */}
      <div className="stage">                            {/* 中央にコンテンツを配置する領域 */}
        <img
          className={`ghost ${visible ? 'show' : 'hide'} ghost-img`}
                                                         // visibleに応じて show/hide クラスを付与(CSSでフェード)
          src={ghostImg}                                 // import した画像のパス
          alt="おばけ"                                   // 画像の代替テキスト(アクセシビリティ対応)
        />
      </div>

      <button
        className={`bottom-btn ${pressed ? 'is-pressed' : ''}`}
                                                         // 押している間だけ is-pressed を付与(押し込みアニメ)
        onClick={toggle}                                 // クリックで 👻 の表示/非表示を切り替え
        onPointerDown={() => setPressed(true)}           // マウス/タッチ/ペン:押した瞬間に“押下中”開始
        onPointerUp={() => setPressed(false)}            // 離したら“押下中”終了
        onPointerLeave={() => setPressed(false)}         // 指やカーソルが外に出たら終了(押しっぱなし対策)
        onPointerCancel={() => setPressed(false)}        // OS側でキャンセルされた場合も終了
        aria-label={visible ? 'いないいな〜い' : 'ばあっ!'} // 画面読み上げ向けラベル(状態に合わせた文言)
      >
        {visible ? 'いないいな〜い' : 'ばあっ!'}         {/* ボタンの表示テキスト(状態に応じて切替) */}
      </button>
    </div>
  )
}

src/App.css(中央配置・フェード・押し込み・白背景)

:root { color-scheme: light; }               /* OSのダーク設定に影響されないよう“ライト”固定 */
html, body, #root, .wrap {
  height: 100%; margin: 0;                   /* 余白ゼロ & 全高を確保して縦レイアウトに使う */
  background: #fff; color: #111;             /* 全体を白背景に、文字は濃いグレー */
}

.wrap {
  display: flex; flex-direction: column;     /* 上: コンテンツ / 下: ボタン の縦並び */
}

.stage {
  flex: 1;                                   /* 余白をすべてこの領域に広げて中央寄せを実現 */
  display: grid; place-items: center;        /* CSS Grid で、子要素を縦横ど真ん中に */
  padding: 16px;
}

/* 👻 のフェード(透明度のアニメーション) */
.ghost { transition: opacity 600ms ease; }   /* 透明度が 0.6 秒で滑らかに変化 */
.ghost.show { opacity: 1; }                  /* 表示状態 */
.ghost.hide { opacity: 0; }                  /* 非表示状態(スペースは維持) */

.ghost-img {
  width: 220px; height: 220px;               /* 画像サイズを揃える(縦横比は object-fit で保持) */
  object-fit: contain;
}

/* 画面下部いっぱいのボタン&押し込みアニメ */
.bottom-btn {
  position: sticky; bottom: 0;               /* ビューポート下に張り付く(モバイルで便利) */
  width: 100%;
  padding: 18px 20px;
  font-size: 18px;
  border: none;
  background: #111; color: #fff;             /* くっきりした配色 */
  cursor: pointer;

  transition: transform 120ms ease, box-shadow 120ms ease; /* 押し戻りを滑らかに */
  transform: translateY(0);                  /* 平常時の位置 */
  box-shadow: 0 6px 0 rgba(0,0,0,0.35);      /* 影を付けて“浮いている”印象に */
}

.bottom-btn.is-pressed {                      /* React 側の pressed=true に対応 */
  transform: translateY(3px) scale(0.98);    /* 下に 3px 移動+わずかに縮小 → 押し込まれた感じ */
  box-shadow: 0 3px 0 rgba(0,0,0,0.35);      /* 影を浅くして沈んだ印象 */
}

/* 動きを減らしたいユーザー設定への配慮(任意だが推奨) */
@media (prefers-reduced-motion: reduce) {
  .ghost, .bottom-btn { transition: none; }  /* アニメーションを無効化 */
}

用語ミニ辞典(初心者向け)

  • HMR(Hot Module Replacement):保存時にページをリロードせず差分だけ更新する仕組み。素早い開発体験に必須。
  • Pointer Events:マウス・タッチ・スタイラスを”ひとつのAPI”で扱うイベントモデル(onPointerDown/Up/...)。
  • 擬似クラス :active:押している“瞬間だけ”適用される CSS の疑似クラス(今回、より確実さ重視で状態管理を採用)。
  • マルチステージビルド:Docker で「ビルド用の段階」と「本番実行用の段階」を分け、最終イメージを小さく安全にする手法。
  • Publish Directory(Render):静的サイトの公開対象フォルダ。Vite の場合は dist

つまずきやすいポイントと即チェック

  • 画像が出ないsrc/assets の画像は import して <img src={...}>public/ghost.png を使うなら src="/ghost.png"
  • HMRが効かないvite.config.tsserver.host=true、compose の ports/volumes、起動オプション --host 0.0.0.0 を再確認。
  • RenderでNot found:Static Site なら Publish Directory=dist、Web Service なら $PORT にバインド

学習に“必ず一読”しておくと理解が深まる公式ドキュメント

  • Vite:Guide(Getting Started / Static Asset Handling / Deploying a Static Site)
  • React:Quick Start(state とイベント)、TypeScript と React の使い方
  • Docker:Compose file リファレンス(services/ports/volumes)、Dockerfile リファレンス
  • Render:Static Sites(Build Command/Publish Directory)、Redirects & Rewrites(SPA用)

検索ワード例

  • vite assets import public path / vite server host 0.0.0.0 docker hmr
  • react useState pointer events onPointerDown
  • docker compose services ports volumes
  • render static site publish directory dist rewrite index.html

Discussion