👶
息子専用アプリ開発①(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.ts
のserver.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