2024-3I プログラミング3 第04回 講義資料

2024年10月24日(木)1・2時限

1 連絡1

2 連絡2

本科目は学修単位科目です。1回の授業あたり、皆さんが「4時間相当の授業時間外学習」をすることを前提 としたボリュームと展開速度となっています。1週間の間隔をあけての90分だけの取り組みで理解・習得できる内容ではないので注意してください。

初回授業から後期中間試験ぐらいまでの授業の流れ (予定)

  1. モダンTypeScript基礎学習のための環境構築
  2. TypeScript基礎学習
    • React / Next.js 開発に関連する文法や機能だけを集中的に学びました。
  3. Reactを使ったTodoアプリのための環境構築 ← 今回の授業 (前半)
  4. Todoアプリ開発(Reactによるフロントエンド開発)のチュートリアル ← 今回の授業 (後半)、次回授業
  5. Todoアプリのカスタマイズや作り込み → 後期前半の大課題 (宿題)

今回講義の 後半の内容 は、前回講義で宿題として示した【React入門】完全初心者OK!1から簡単なTodoアプリを作ってReactの1歩を踏み出してみよう ~Reactチュートリアル~の視聴を前提とした内容です。

3 React開発環境の構築

Viteというビルドツールを使用して「Tailwind CSS」と「TypeScript」を用いた Reactアプリの開発環境を構築する手順 について解説します。

3.1 準備

ターミナル (PowerShell) を開いてプロジェクトフォルダを配置したいフォルダ (例えば C:\Users\xxxx\Documents\ など) に移動してください。いまから、このフォルダのなかに react-todo-app というプロジェクトフォルダを作成して、そこに「React (TypeScript+Tailwind CSS) の開発環境」を構築していきます。

3.2 Viteによるプロジェクトフォルダの作成

ここでは、Vite という「ウェブ開発用のビルドツール」を使って環境を構築していきます。

Vite を使用する方法以外にも npx create-react-app というコマンドでも同様に環境構築が可能ですが、こらちは長らくアップデートされていないので、ここでは Vite を使用するものとします。

ターミナル (PowerShell) 以下のコマンドを実行してください。

npm create vite@latest react-todo-app -- --template react-ts

コマンドオプションの react-todo-appプロジェクトの名称 (=プロジェクトフォルダの名称) を指示しています。また -- は後続オプションを (npm ではなく) Vite に渡すことを意味し、--template react-ts では React と TypeScript を統合した開発環境テンプレート を使って環境構築することを指示しています。

なお、npm create vite@latest を初めて実行するときや、前回実行から時間が経っているときは、以下のように尋ねられるので、続行するために y を入力してください。

Need to install the following packages:
create-vite@5.5.3
Ok to proceed? (y)

補足

コマンドオプションに --template react-ts を付けているので表示されないと思いますが、もし「Select a framework」と聞かれたら「React」、「Select a variant」と聞かれたら「TypeScript」を選択してください。

次のように応答が返ってくればコマンドオプションの react-todo-app作成するプロジェクトの名称 (=プロジェクトフォルダの名称)、 -- は後続オプションを npm ではなく Vite に渡すことを意味するもので、--template react-ts によって React と TypeScript を統合した開発環境 (テンプレート) を指示します。成功です。

Scaffolding project in C:\Users\xxxx\Documents\react-todo-app...

Done. Now run:

  cd react-todo-app
  npm install
  npm run dev

3.3 プロジェクトフォルダの構成確認

作成されたフォルダ (react-todo-app) に cd コマンドでに移動して VSCode を立ち上げてください。

cd react-todo-app
code .

VSCodeのエクスプローラから、フォルダ構成が次のようになっていることを確認してください。

img

プロジェクトフォルダに配置される各ファイルの概要は以下のようになります。

3.3.1 tsconfig.**.json

プロジェクトフォルダ内の「tsconfig.json」「tsconfig.app.json」「tsconfig.node.json」の3ファイルはいずれも TypeScript関連の設定ファイル となります。「tsconfig.json」から「tsconfig.app.json」と「tsconfig.node.json」を参照する構成になっています (実際にVSCodeで「tsconfig.json」の内容を確認してください)。

3.3.2 README.md

README.md」は、GitHubのリポジトリのトップページに表示される「README」を記述するファイルになります。マークダウン形式 (.md拡張子) で記述します。

特に、プロジェクトを「ポートフォリオに位置づける場合」には、アプリ概要 (機能一覧) やライセンス情報の他に 開発の経緯(なぜそのアプリを制作したのか)、技術スタック (プロジェクトで使用している「フレームワーク」「ライブラリ」「ツール」など)こだわりのポイント を記載するようにしてください。

3.3.3 .gitignore

.gitignore」は、Gitを使ったバージョン管理を「しない」ファイルやフォルダを設定するためのファイルになります。バージョン管理をしない = GitHub にもアップされない ということになります。

.gitignore」の内容を確認すると、パッケージ本体を格納しているnode_modules フォルダのほか、DB接続情報やAPIをキーを格納するファイル ( env.local ) などが、Git管理から除外されるようになっています (=GitHubにアップにされないようになっています)。

3.3.4 eslint.config.js

eslint.config.js」はESLint という コード品質チェックツール に関する設定ファイルになります。例えば、ESLintを使うと 宣言されているものの未使用の変数 があるときなどに「警告」や「エラー」として検出してくれます。

なお、「Lint」は (TypeScriptに限らず) プログラミング一般において「ソースコードを解析し、バグやスタイルの問題、疑わしい構造などを検出するプロセス」を指し、そのためのツールが Linter(リンター) と呼ばれています。C言語 や Python用の Linter もあります。ESLint は、特に ECMAScript (JavaScript / TypeScript) 用の標準Linterとして知られています。

この授業でのReact開発には、フォーマッターとしては「Prettier (プリティア)」、リンターとしては「ESLint (イーエスリント)」を使用していきます。両者の「役割」や「機能」の違いが気になる人は、生成AIで解決してください。

(プロンプト例)

React (TypeScript) 開発の際に、フォーマッターには「Prettier」を、リンターには「ESLint」を使用するように勧められました。両者の役割や機能の違いについて教えてください。

3.4 パッケージ (ライブラリ) のインストール

プロジェクトフォルダのなかの「package.json」の内容を確認してください。

第01回講義で解説したように、package.json は アプリの開発や実行にに必要なパッケージ (ライブラリ) の情報 や、開発に使用するコマンドをショートカットとして定義 する設定ファイルとなります。

package.json のなかの "dependencies""devDependencies" の項目を確認してください。TypeScript を用いて React開発するために 最低限必要となるパッケージ (ライブラリ) が記載されています。

ただし、現時点では、まだパッケージ (ライブラリ) の実体はインストール されていません (プロジェクトフォルダのなかに「node_modules」フォルダが存在しません)。いまから package.json に記載されたパッケージをプロジェクトフォルダにローカルインストールしていきます。

VSCode上から [Ctrl]+[J] のショートカットでターミナルを開いて npm install または npm i を実行してください。これにより、package.json に基づきパッケージがインストールされます。

npm i

ファイルのダウンロードなどがあるので、この処理は少し時間がかかります。

完了するとプロジェクトフォルダのなかに node_modulesフォルダpackage-lock.json が追加されます。「package-lock.json」については第01回講義で解説済みですが、詳しく知りたい人は生成AIで解決してください。

(プロンプト例)

Node.js環境における package.jsonpackage-lock.json の違いについて詳しく教えてください。

3.5 Reactアプリのビルドと動作確認

ここまでの作業で、Vite によって作成されたアプリ (のひな形) を動作確認するための準備が整いました。ターミナルに npm run dev コマンドを入力して、開発モード で Reactアプリを動かしてみてください。なお、npm run dev によって実際に実行されるコマンドは「package.json」の "scripts" の項目に記載されているように vite となります。

npm run dev

ターミナルに次のように表示がされ、ローカルホストの 5173 番ポート でアプリが立ち上がります。

PS C:\Users\xxxx\Documents\react-todo-app> npm run dev     

> react-todo-app@0.0.0 dev
> vite

  VITE v5.4.9  ready in 420 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

ウェブブラウザでhttp://localhost:5173/にアクセスしてください。次のように表示されるはずです。また、画面内の「count is 0」のボタンを押下して 「数値」がカウントアップ することを確認してください。

このカウントアップは React の useState という主要な機能を利用して行なわれています。数値のカウントアップは 「Hello World」や「(組込み開発の)Lチカ」のような位置づけ になります。

img

アプリの動作確認ができたら「停止」してください (VSCodeのターミナルをアクティブにして [Ctrl]+[C] を押下してください)。

上記では「開発モード」を使ってアプリを実行しましたが、今度は プレビューモード (疑似本番モード) でアプリを実行してみたいと思います。まずは、ターミナルに npm run build を入力して「ビルド」を行ないます。プロジェクトフォルダのなかに distフォルダ が新たに追加されて、そのなかにビルドされたファイルが格納されます。

npm run build
img

dist フォルダに出力されたアプリを実行するためには npm run preview コマンドを入力します。

npm run preview

今度はアプリが 4173番ポート で立ち上がるので、ウェブブラウザからhttp://localhost:4173/にアクセスして動作を確認してください。「開発モードでの実行」と「プレビューモードでの実行」の違いについては、ウェブ検索や生成AIの解説を参照してください。

(プロンプト例)

ビルドツールにViteを使用してReact開発をしています。npm run dev でアプリを実行したときと、 npm run build して npm run preview でアプリを実行したときの違いを教えてください。

プレビューモードでの動作を確認できたら、[Ctrl]+[C]アプリを停止 しておいてください。

3.6 不要なファイルや記述を削除

Reactについて要点を押さえて理解するために、プロジェクトフォルダから不要なファイルを削除し、いくつかのファイルについて不要な記述を削除してしていきます。

3.6.1 index.html の編集

index.html を次のように編集してください。<html> タグと <title> タグの部分を書き換えています。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TodoApp</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

その他の箇所についても、リンターやフォーマッターによって「タグの大文字小文字」や「引用符」が書き換わることがありますが、そこは気にする必要はありません (以降も同様)。

最後に「保存すること」を忘れないでください。

3.6.2 src/App.tsx の編集

src/App.tsx を次のように編集してください。拡張子 .tsx は、React用の JSX(XMLライクな拡張構文)および TypeScript に対応したプログラムが記述されていることを示しています。このプログラムの内容について後で解説するので、現時点では深く理解する必要はありません (第12行目 でボタンをクリックすすると onClick={countUp} によって、第05行目でアロー関数形式で定義している countUp が呼び出されるのかな🤔…ぐらいの簡単な理解で十分です)。

まずは、開発環境の構築を優先して進めます。

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const countUp = () => {
    const newCount = count + 1;
    setCount(newCount);
  };
  return (
    <div>
      <h1>TodoApp</h1>
      <button onClick={countUp}>count is {count}</button>
    </div>
  );
};

export default App;

さきほどと同様に、最後に「保存すること」を忘れないでください。

3.6.3 ファイルとフォルダの削除

このあと Tailwind CSS を導入することを前提に CSS 関連のファイルとフォルダを削除します。

Reactのロゴが描かれたファイル (src/asset/react.svg) も不要なので、src/asset フォルダごと削除してください。

img

3.6.4 README.md の編集

README.md を次のように書き換えてください。

第13行目Copyright (c) 2024 Kosen Taro については、適当に書き換えてください (名前の部分はハンドルネームでOKです)。

# TodoApp

React、TypeScript、Tailwind CSS を使用し、ローカルストレージでデータを永続化した「Todoアプリ」です。

## 開発履歴

- 2024年10月24日:プロジェクト開始

## ライセンス

MIT License

Copyright (c) 2024 Kosen Taro

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3.7 動作確認 (CSSの剝ぎ取り)

次のコマンドを実行して、開発モードで動作確認します。

npm run dev

CSS関連の設定を削除したので、次のようなシンプルなアプリ画面になっていることが確認できると思います。ただし、ボタンを押下したときのカウントアップ は引き続き機能することを確認してください。

img

動作確認ができたら、停止しておいてください。

3.8 Tailwind CSS の適用

この授業ではTailwind CSSという CSSフレームワーク を標準的に使用します (既に他のCSSフレームワークを使い慣れている人は、そちらを利用してもらっても問題ありません)。TailwindCSS は、あらかじめ用意されている 細かく分割されたデザインの部品 を組み合せてUIをスタイリングする点に特徴あります。

例えば「青い背景色 bg-blue-500」「4単位量の左右余白 px-4」「四隅を丸める rounded」といったデザイン部品の組み合わせを <button className="bg-blue-500 px-4 rounded"> のように与えてスタイリングします。CSSクラスの名前を考えたり、スタイルの衝突を心配する必要がないので開発速度がアップし、デザインの一貫性や保守性 も保ちやすくなります。なお、Next.js については、標準で Tailwind CSS をサポートしており、様々なプロダクトで採用されています。

次のコマンドを実行して「tailwindcss」「postcs」「autoprefixer」という3つのパッケージをインストールしてください。Tailwind CSS で設定されたスタイルは、ビルド時に通常のCSSに変換されるため、いずれのパッケージも「開発時のみ」に必要となります(本番環境での実行時には使用しません)。そのため -D オプションを付加して開発依存関係としてインストールしています。

3.8.1 パッケージのインストール

次のコマンドを実行して「tailwindcss」「postcs」「autoprefixer」という3つのパッケージをインストールしてください。Tailwind CSS で設定されたスタイルは、ビルド時に通常のCSSに変換されるため、いずれのパッケージも「開発時のみ」に必要となります (本番環境での実行時には使用しません)。そのため -D オプションを付加して開発依存関係として扱います。

npm i -D tailwindcss postcss autoprefixer

3.8.2 設定ファイルの追加

プロジェクトに Tailwind CSS を適用するために設定ファイルを追加して、既存のファイルを書き換えます。

まず、プロジェクトフォルダ直下 ( src の内部ではないので注意! ) に tailwind.config.js を新規作成して、次の内容を貼付けて保存してください。

/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

同様に postcss.config.js を新規作成して、次の内容を貼付けて保存してください。

export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

src/index.css (空ファイルだったもの) に、次の内容を記述してください。以下、警告マークがでますが、一旦、無視してください。

@tailwind base;
@tailwind components;
@tailwind utilities;

なお、上記の src/index.css ファイルは、src/main.tsx第04行目 で読み込まれ、アプリ全体で Tailwind によるスタイリングが有効 になります。

各ファイルとも保存されていることを再確認してください。

この状態で npm run dev で動作確認すると、デフォルトのスタイルを含めて、全ての CSS が剝がされた状態のアプリ画面になります。境界線が無くなって分かりづらいですが「count is 0」の文字を押下すると、カウントアップは問題なく機能します。

3.8.3 TailWind によるスタイリング

Appコンポーネントが記述されている src/App.tsxTailwind CSS によるスタイリング を適用していきます。次のように書き換えてください。

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const countUp = () => {
    const newCount = count + 1;
    setCount(newCount);
  };
  return (
    <div className="mx-4 md:mx-auto mt-10 max-w-2xl">
      <h1 className="font-bold text-2xl mb-4">TodoApp</h1>
      <button
        className="bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded-md"
        onClick={countUp}
      >
        count is {count}
      </button>
    </div>
  );
};

export default App;

書き換えたのは 第10行目 から 第15行目 の範囲 (return しているJSX構文の内部) になります。

いずれも HTMLタグ (厳密にはJSXタグ) に className 属性を追加して、そこに Tailwindのクラス (mx-4font-bold など) を記述しています。HTMLタグでは class 属性にCSSクラスを記述しましたが、JSXタグ (.jsx.tsxファイルで利用可能な構文) では className 属性にクラスを記述します。

HTMLとCSS

HTML」と「CSS」の基礎は、情報3の「第11回講義」と「第12回講義」で学習済みです。

以下のように、CSSファイル main.css を作成し、そこに「CSSクラス」を独自に定義して <div class="container" > のように class 属性を使ってHTML要素のスタイリングをしました。

<!DOCTYPE html>
<html lang="ja" >
  ~~~ 略 ~~~
<body>
    <div class="container" >
      <h1>XXX's Curry Recipe</h1>
      ~~~ 略 ~~~
    </div>
</body>
</html>
.container { 
  max-width: 768px;
  width: 100%;
  margin: 0 auto;
} 

以上で Tailwind CSS の適用が完了しました。

npm run dev で動作確認してください。

ボタンにマウスカーソルを重ねると (ホバーすると) 色が変わることや、ウィンドウ幅を変えても左右 (X方向) の余白が適切に調整されること も確認してください。また、レスポンシブデザインになっていることも確認してください (デベロッパツールでモバイル表示に切り替えてみてください)。

img

3.8.4 演習

開発モード (npm run dev) でアプリを実行しているときは ホットリロード が有効になります。つまり、コードを書き換えて保存すると、その結果が自動でウェブブラウザに反映されます

実際に開発モードでアプリを起動し、以下のように src/App.tsx のコードを書き替えて保存すると、それが どのように画面に反映されるか を確認してください。

参考: TailwindCSSの色指定の一覧

3.8.5 Tailwind スタイルの調べ方

className 属性を使って設定する各種スタイル (ユーティリティクラス) については、都度、解説していきますが、自分で調べたいときはTailwindの公式ページや生成AIを活用してください。

(プロンプト例)

TailwindCSS で、次のようにスタイルが適用されていました。各CSSクラスの意味について解説してください。
<div className="mx-4 md:mx-auto mt-10 max-w-2xl">

(プロンプト例)

TailwindCSS でボタンに「ドロップシャドウ」を適用したいです。どのようにすればよいですか。
<button className="bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded-md">ほげ</button>

4 ポート設定のカスタマイズ

Viteで構築したプロジェクトは、デフォルト設定では 開発モードnpm run dev で実行)が 5173番ポート で起動します。このポート番号は vite.config.ts から任意に設定ができます。後期中間以降に利用するNext.jsでは開発サーバが 3000番ポート で起動するので、それにあわせて、ここでも 3000番ポート で開発サーバが起動するように設定を変更してみます。

プロジェクトフォルダの直下にある vite.config.ts を、次のように変更してください。

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000, // デフォルトのポートを3000に設定
    strictPort: false,
    open: true,
  },
});

ターミナルから npm run dev を実行し 3000番ポート で開発サーバが起動することを確認してください。

PS C:\Users\xxxx\Documents\react-todo-app> npm run dev

> react-todo-app@0.0.0 dev
> vite


  VITE v5.4.9  ready in 1004 ms

  ➜  Local:   http://localhost:3000/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

また open: true の設定により ウェブブラウザも自動起動する ようになっています。さらに strictPort: false の設定により、3000番ポートが他のアプリで既に使われている場合、3001番ポートでの起動が試されます。同様に、3001がダメなら、3002、3003…と起動が試行されます。

5 ESLintのカスタマイズ

ESLint 関連のカスタマイズについて簡単に説明しておきます。src/App.tsx を開いて、以下のように 第04行目const hoge = "Hoge"; というコードを追加してください。

import { useState } from "react";

const App = () => {
  const hoge = "Hoge"; // ■■■ 追記 ■■■
  const [count, setCount] = useState(0);
  const countUp = () => {
    const newCount = count + 1;
    setCount(newCount);
  };
  return (
    <div className="mx-4 md:mx-auto mt-10 max-w-2xl">
      <h1 className="font-bold text-2xl mb-4">TodoApp</h1>
      <button
        className="bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded-md"
        onClick={countUp}
      >
        count is {count}
      </button>
    </div>
  );
};

export default App;

すると、追記した const hoge = "Hoge" の部分で次のように「エラー」が発生します🤮。

‘hoge’ is assigned a value but never used. eslint@typescript-eslint/no-unused-vars
‘hoge’ が宣言されていますが、その値が読み取られることはありません。ts(6133)

img

ただし、この状態であっても 開発モード (npm run dev) で起動した開発サーバ では 問題なくページが表示 されます。これは、このエラーが「ESLintとしてのエラー」であって「実行時エラー (Runtime Error)」ではないためです。

一方で、ビルド(npm run build)を実行すると、明確なエラーとして処理が中断されてしまいます。当然、dist フォルダにもビルドしたファイルが出力されません。

このように 開発モードでの実行には問題がないものの、本番用にビルドすると失敗する ということは多々あります。そのため、VSCodeでエラー(赤色波線)になっている箇所は放置せずに修正し、また定期的に npm run build を実行することをお勧めします。アプリが完成したと思っていたのに、大量のビルドエラーでデプロイに失敗すると精神的なダメージが大きいです。

・・・とはいえ「hogeが宣言されていますが、その値が読み取られることはありません」のような軽微なコード規約違反がエラー扱いになると、開発途中では非常に不便です(特に個人開発という場面では)。例えば npx create-next-app@latest コマンドで作成する「Next.jsの開発環境」では 変数の未使用は「エラー」ではなく「警告」という扱い になっています(警告レベルなのでビルドも可能です)。このように、Viteで構築されるデフォルト環境のESLintの設定は少し厳しいもの になっています。

ということで、「変数の未使用」が「エラー」ではなく「警告」の扱いになるようにESLintの設定をカスタマイズ していきます。

まず tsconfig.app.json について、次のように変更します (tsconfig.node.json ではないので注意してください)。"noUnusedLocals""noUnusedParameters" の項目を変更してください。

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": false,  /* 変更 */
    "noUnusedParameters": false,  /* 変更 */
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}

つづいて eslint.config.js に、'@typescript-eslint/no-unused-vars': 'warn', の設定を追加してください。追加時には、オブジェクトリテラルのフォーマットに従い、末尾にカンマを付けること を忘れないようにしてください。

import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      '@typescript-eslint/no-unused-vars': 'warn', // 追加
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
    },
  },
)

eslint.config.js を変更して保存するときには、少し時間がかかることがあります(おそらく全てのコードが再チェックされるためだと思います)。

以上の設定を済ませると src/App.tsxconst hoge = "Hoge"; のところが警告の扱い(赤色の波線から黄色の波線の表示)に変わっていることが確認できると思います。また npm run build を実行した際も、エラーで中断されることなくビルドが完了すると思います。

このようにLint関連のカスタマイズは eslint.config.jstsconfig.app.json でできることを知っておくと役立と思います。

5.1 Tailwind CSS 関連のLint

Tailwindを使用する場合、その特性上、次のようにJSXタグの className 属性に多数のユーティリティクラスを列挙することになります。

<h1 className="font-bold text-2xl mb-4">TodoApp</h1>

リンターであるESLintの設定フォーマッターであるPrettierの設定 をカスタマイズすることで、これらのクラスについて 自動で並び替えすること が可能になります(コードの「可読性」と「一貫性」が向上します)。

まず、VSCodeの拡張機能としての Prettier (識別子:esbenp.prettier-vscode) とESLint (識別子:dbaeumer.vscode-eslint) はあらかじめインストールされているものとします。

そのうえで、次のように「prettier」と「eslint-plugin-tailwindcss」という開発用のパッケージをインストールします。

npm i -D prettier eslint-plugin-tailwindcss

つづいて eslint.config.js を次のように変更してください。

import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import tailwind from "eslint-plugin-tailwindcss"; // 追加

export default tseslint.config(
  { ignores: ["dist"] },
  {
    extends: [
      js.configs.recommended,
      ...tseslint.configs.recommended,
      ...tailwind.configs["flat/recommended"], // 追加
    ],
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      "react-hooks": reactHooks,
      "react-refresh": reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      "@typescript-eslint/no-unused-vars": "warn",
      "react-refresh/only-export-components": [
        "warn",
        { allowConstantExport: true },
      ],
    },
  }
);

さらに、プロジェクトフォルダの直下.vscode フォルダを作成して、そのなかに settings.json を作成して以下を記述してください。setting.json ではなく settings.json なので注意してください。なお、最後の "css.lint.unknownAtRules": "ignore" の設定によって src/index.css で表示されていたエラーが抑制されます。

{
  "editor.formatOnSave": true,
  "editor.formatOnPaste": true,
  "editor.formatOnType": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": ["source.fixAll.eslint"],
  "css.lint.unknownAtRules": "ignore"
}

以上で設定が完了です。

VSCodeで src/App.tsx を開くと、以下のように className 属性に警告(黄色の波線)が表示されています。これはTailwindユーティリティクラス 並び順がよくないよ という警告です。

img

この警告状態のまま、保存操作をすると次のように自動でクラスの並び替えをしてくれます。

img

以上で、基本的な環境構築は完了です。

5.1.1 演習 (宿題: 30分)

プロジェクトフォルダをリネームするか削除して、再度、環境構築に取り組み、大まかな手順を覚えてください。可能であれば、3回、4回と繰り返してください (慣れれば、5分程度で一連の環境構築が可能になります)。

6 Reactアプリケーションの実行順序の理解

Viteで構築したReactプロジェクトを 開発モード (npm run dev) で実行したときに、各ファイル (各コンポーネント) が読み込まれていく順番について解説します (特にここでは「分かりやすさ」を優先するため、厳密な説明ではない部分があるので注意してください)。

まず一番初めにプロジェクトフォルダ直下にある index.html が読み込まれます。このようにプログラム実行の開始点 (起点) となるものを エントリーポイント といいます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TodoApp</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

次に、上記の 第11行目 に記述される <script type="module" src="/src/main.tsx"></script> によって /src/main.tsx が読み込まれます。

そして main.tsx第06行目document.getElementById('root') によって、index.html第10行目<div id="root"></div> の領域(idが root のdiv要素)に対して <StrictMode><App /></StrictMode> というコンポーネントがレンダリング (描画) されます。

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

ここでレンダリングされる <App /> コンポーネントの実体は src/main.tsx第03行目 で読み込んでいる App.tsx となります。なお、<StrictMode>...<StrictMode> コンポーネントについては、いまは無視してください (気になる人は検索か生成AIで解決してください)。

そして、以下のような App.tsx によって、「TodoApp」という見出しや「count is 0」というボタンが画面に表示され、また 第05行目 の関数 countUp によってカウントアップが機能します。

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const countUp = () => {
    const newCount = count + 1;
    setCount(newCount);
  };
  return (
    <div className="mx-4 mt-10 max-w-2xl md:mx-auto">
      <h1 className="mb-4 text-2xl font-bold">TodoApp</h1>
      <button
        className="rounded-md bg-blue-500 px-4 py-0.5 text-white hover:bg-blue-700"
        onClick={countUp}
      >
        count is {count}
      </button>
    </div>
  );
};

export default App;

以上、整理すると開発モードでは、以下のような順番でファイルが読み込まれていくと考えてください (プログラムがネスト構造になっていると考えてください)。

  1. index.html が読み込まれ、その内部で …
  2. src/main.tsx が読み込まれ、その内部で …
  3. src/App.tsx (Appコンポーネント) が読み込まれる。

また、この先で Appコンポーネントのなかに、TodoListコンポーネント (src/TodoList.tsx) を配置して、さらに、その内部に TodoItemコンポーネント (src/TodoItem.tsx) を配置していきます。その場合は以下のようになります。

  1. index.html が読み込まれ、その内部で …
  2. src/main.tsx が読み込まれ、その内部で …
  3. src/App.tsx (Appコンポーネント) が読み込まれ、その内部で …
  4. src/TodoList.tsx (TodoListコンポーネント) が読み込まれ、その内部で …
  5. src/TodoItem.tsx (TodoItemコンポーネント) が読み込まれる。

6.1 ビルド (バンドル) 出力

ビルド (npm run build) を実行すると、最適化されたプロダクトが「distフォルダ」に出力されます。

このプロダクトは トランスパイル (TypeScript\(\to\)JavaScript)、最適化、バンドル などの処理によって ロジックとファイルが集約されたもので、index.html をエントリポイントとして assets/index-XXXX.jsassets/index-XXXX.css が読み込まれるシンプルな構成になります。

img

実際にビルドを実行して「distフォルダ」の各ファイルの内容について確認してみてください。

img

7 Reactアプリの開発

7.1 Reactにおけるコンポーネントとは

Reactアプリは、基本的に「コンポーネント」とよばる 機能とUIを持った画面構成の部品・パーツ を組み合わせてアプリを構築する点に特徴があります。コンポーネントとは、具体的には .tsx という拡張子を付けたファイルに記述した「JSX要素を戻り値とする関数」を指します。そして、JSX とは、JavaScript XML の略で、JavaScript (TypeScript) プログラムのなかに HTMLのようなタグ を記述できるようにした拡張構文を指します。

例えば「ウェルカムメッセージを表示するようなコンポーネント」をTodoアプリに組み込みたいときは、以下のように src/WelcomeMessage.tsx を作成します。コンポーネントを記述したファイルは、大文字からはじまるパスカルケースで名前を設定します。

import React from "react";

// 引数の型を定義
// Propsという名前で定義することが一般的です。
type Props = {
  name: string;
};

// WelcomeMessage という関数コンポーネントの定義
// 関数コンポーネントはパスカルケースで名前を設定します。
const WelcomeMessage = (props: Props) => {
  // いわゆる普通のロジックを記述する
  const currentTime = new Date();
  const greeting =
    currentTime.getHours() < 12 ? "おはようございます" : "こんにちは";

  //【重要!】JSX構文で描いた「JSX要素」を return で返す
  return (
    <div className="text-blue-700">
      {greeting}、{props.name}さん。
    </div>
  );
};

// 他のファイルで WelcomeMessage を import できるようにする
export default WelcomeMessage;

そして、src/App.tsx なかで以下のようにインポートして組み込みます。

import { useState } from "react";
import WelcomeMessage from "./WelcomeMessage"; // 追加

const App = () => {
  const [count, setCount] = useState(0);
  const countUp = () => {
    const newCount = count + 1;
    setCount(newCount);
  };
  return (
    <div className="mx-4 mt-10 max-w-2xl md:mx-auto">
      <h1 className="mb-4 text-2xl font-bold">TodoApp</h1>
      <WelcomeMessage name="寝屋川タヌキ" /> {/* 追加 */}
      <button
        className="rounded-md bg-blue-500 px-4 py-0.5 text-white hover:bg-blue-700"
        onClick={countUp}
      >
        count is {count}
      </button>
    </div>
  );
};

export default App;

第02行目で「WelcomeMessageコンポーネント」を読込み、第13行目<WelcomeMessage name="寝屋川タヌキ" /> のように組み込んでいます。ここでは name という引数が、どのようにWelcomeMessageコンポーネントに渡されているか (対応関係) をよく確認してください。例えば「name="寝屋川タヌキ" ではなく userName="寝屋川タヌキ" のように名前をウェルカムメッセージコンポーネントに渡したいときは、WelcomeMessage.tsx のどこを書き換えるべきか」が分かるでしょうか?

なお、JSX要素のなかでは、コメントは {/* コメント */} のように記述します。

Reactの関数コンポーネントはパスカルケースで名前を設定すること

TypeScriptでは、通常、関数名には キャメルケース (小文字から開始) を使用します。ただし、Reactの関数コンポーネントは、パスカルケース (大文字から開始) を使用する必要があります (また、これにあわせてファイル名も キャメルケース にします)。この命名規則から外れていると、次のように JSX構文のなかでコンポーネントが適切に認識されません。注意してください。

img

7.1.1 演習

実際に「WelcomeMessageコンポーネント」を作成して、「Appコンポーネント」のなかに組み込んで組み込んでください。また、開発モードで実行結果を確認してください。

また、以下の要求を満たすようにプログラムを書き換えてください。

7.2 状態管理

Reactアプリでは アプリ画面上で変化する値状態(State) という特別な仕組みで管理します。この「状態」が更新されると、Reactが変更を検知し、関連するコンポーネントだけを自動的に再レンダリング (つまり再描画して値の変更を反映) します。

何らかの値 (例えば「現在のカウント」) を「状態」として扱い、その変更に応じて画面を再レンダリングするには、通常の変数として定義するのでは不十分であり、フックス (Hooks) という機能を利用して値を管理 (保持・更新) することが求められます。Reactには、状態管理のために様々な Hooks が用意されていますが、そのなかでも特に基本的な役割を果たすものが useState という フック (Hook) になります。

サンプルとして示したアプでは、カウント値が更新されたときに、それを即座に画面に自動反映させたいので、第04行目 のように useState を使って、カウント値を保持する変数 count と、それを更新するための専用関数 setCount を導入しています。なお、useState の引数には初期値を与えます (例えば、カウントを「7」から開始したいときは const [count, setCount] = useState(7); とします)。

そして、現在のカウント値を得たいときは count を参照し、更新したいときは setCount(newCount) を呼び出すことで、意図するようなカウント動作が実現されます。

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0); // ここに注目
  const countUp = () => { 
    const newCount = count + 1;
    setCount(newCount); // ここにも注目。
  };
  return (
    <div className="mx-4 mt-10 max-w-2xl md:mx-auto">
      <h1 className="mb-4 text-2xl font-bold">TodoApp</h1>
      <button
        className="rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-700"
        onClick={countUp}
      >
        count is {count}
      </button>
    </div>
  );
};

export default App;

一方で、以下のように useState を使用せずに実装するとボタンを押下しても画面が更新されません (=カウント表示が変わらない)。実際に試してみてください。

const App = () => {
  let count = 0; // 通常の変数として定義
  const countUp = () => {
    count++; // 通常の処理で更新 count = count + 1; でも同じ
    console.log(`現在の count の値は ${count} です`); // 確認出力
  };
  return (
    <div className="mx-4 mt-10 max-w-2xl md:mx-auto">
      <h1 className="mb-4 text-2xl font-bold">TodoApp</h1>
      <button
        className="rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-700"
        onClick={countUp}
      >
        count is {count}
      </button>
    </div>
  );
};

export default App;

ただし、[F12] でデベロッパーツールを起動して、コンソールタブからログを確認してみると、画面上の「カウント値」が更新されないだけで、内部的には変数 count は正常に更新されていること が確認できます。

img

これらのことから「変更をトリガーに再レンダリングする必要ない値」については通常の変数として扱い、逆に「値の変更をトリガーに再レンダリングしたいもの」は useState を使って「状態」として扱う 必要があることが分かると思います。

7.3 練習

次のように、カウント値の表示部をボタンの外に移動し、ボタンを「1Up」と「10Up」の2個に増やすようにアプリを拡張してみます。

img

まずは、以下のようにプログラムを書き替え、実行結果とプログラムを照らし合わせてプログラムについて理解してください。

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const countUp1 = () => { // 追加
    const newCount = count + 1;
    setCount(newCount);
  };
  const countUp10 = () => { // 追加
    const newCount = count + 10;
    setCount(newCount);
  };
  return (
    <div className="mx-4 mt-10 max-w-2xl md:mx-auto">
      <h1 className="mb-4 text-2xl font-bold">TodoApp</h1>
      <div className="mb-2">
        <p>
          現在のカウント値は
          <span className="text-xl font-bold text-blue-500"> {count} </span>です
        </p>
      </div>
      <div className="flex space-x-2">
        <button
          className="rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-700"
          onClick={countUp1}
        >
          1 Up
        </button>
        <button
          className="rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-700"
          onClick={countUp10}
        >
          10 Up
        </button>
      </div>
    </div>
  );
};

export default App;

このプログラムには冗長な記述が多いのでリファクタリングしていきます。関数 countUp1countUp10 は、以下のように省略形で (1行で) 書き換えが可能です。前回講義で学んだアロー関数形式の応用です。

const countUp1 = () => {
  const newCount = count + 1;
  setCount(newCount);
};
const countUp10 = () => {
  const newCount = count + 10;
  setCount(newCount);
};
const countUp1 = () => setCount(count + 1);
const countUp10 = () => setCount(count + 10);

実際に書き換えて、問題なく動作することが確認してください。

さらに、関数が引数を受け取るように変更して、onClick での呼び出しを onClick={() => countUpN(1)}onClick={() => countUpN(10)} とすると、さらにスマートに記述ができます (全体のコードはこちらを参照)。

const countUpN = (n: number) => setCount(count + n);

また、関数 countUpN を削除してこちらのように onClick直接処理を記述するような実装 もあります。

7.3.1 演習

次のように、カウントをリセットするボタンを実装してください。

img

8 Todoアプリの実装

8.1 準備: ライブラリのインストール

個々の Todo に ID を設定するため UUID (Universally Unique Identifier) v4 を生成可能なuuidというライブラリをインストールします。また、日時の管理を簡単にするためにdayjsdate-fnsというライブラリをインストールします。

VSCodeのターミナルから以下のコマンドを実行してください。

npm i uuid dayjs date-fns

また、uuid ライブラリは (TypeScriptではなく) JavaScript で書かれており TypeScriptに必要な型定義を含んでいない ため、それを補う型定義 (@types/uuid) を追加でインストールします。なお、この型定義ファイルは 開発だけに必要 (実行時は不要) なので -D オプションを付けてインストールします。

npm i -D @types/uuid

アイコン用にFont Awesomeを使用したいので、型定義と共にインストールします。Font Awesome \(\to\)

npm i @fortawesome/react-fontawesome @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons
npm i -D @types/react-fontawesome

Tailwind 関連のユーティリティクラス (font-boldpx-4 など) を動的に結合する際に、スタイリングの衝突を解決してくれるtailwind-mergeをインストールします。開発時のみ必要なので -D オプションを付けてインストールします。

npm i -D tailwind-merge

8.2 準備: 初期データの作成

Todoリストの「初期データ」を準備します。

まずは、このTodoアプリで共通使用する「」を定義しておくためのファイルを src/types.ts を作成してください (「src/types.ts を作成せよ」とは「src フォルダのなかに types.ts を作成せよ」という意味です)。作成したファイルに以下のように Todo型 を定義してください。また 定義した型が、他のファイルで import できるように export を付けてください。なお、このファイルは、単に型定義をしているだけのファイルなので拡張子は .tsx ではなく、.ts にします。

export type Todo = {
  id: string;
  name: string;
  isDone: boolean;
  priority: number;
  deadline: Date | null; // 注意
};

ここでの注意してほしいのは 第06行目 で、プロパティ deadline の型を「Date」もしくは「null」に設定している点です (前回講義の解説を参照)。特に「期限を設定しないTood」については deadlinenull として扱うものとします。

次に、Todoリストの初期データを格納するファイルとして initTodos.ts を作成し、以下のように初期値 initTodos を設定してください。この initTodo他のファイルから参照するので忘れずに export キーワードを付与してください。 データの内容は自由に変更して問題ありませんが、このあとの展開の関係で 3個以上を設定 してください。

なお、第06行目uuid() は UUID v4 の文字列 (string型) を ランダム に生成する関数になります (第02行目 では v4 という関数を uuid という名前でインポートしています)。

import { Todo } from "./types";
import { v4 as uuid } from "uuid"; // v4 を uuid という名前でインポート

export const initTodos: Todo[] = [
  {
    id: uuid(), // UUID v4 を生成してIDにセット
    name: "解析2の宿題",
    isDone: false,
    priority: 2,
    deadline: new Date(2024, 10, 2, 17, 30),
  },
  {
    id: uuid(),
    name: "TypeScriptの勉強 (復習)",
    isDone: true,
    priority: 3,
    deadline: null, // このTodoには期限を設定しない
  },
  {
    id: uuid(),
    name: "基礎物理学3の宿題",
    isDone: false,
    priority: 1,
    deadline: new Date(2024, 10, 11),
  },
];

8.3 TodoListコンポーネントの作成

Todoリストを表示するための「TodoListコンポーネント」を作成します。src/TodoList.tsx を作成して、以下の内容を記述してください。ここでは、練習のためにコピペではなくコードを打ち込んでください。前回講義で紹介しているように ES7+React/Redux/Reac1t-Native snippets をVSCodeの拡張機能としてインストールしていると、次の図のように rafce と打ち込むだけで アロー関数形式のReactコンポーネントのテンプレート が得られます。

img

ここで定義する TodoList は、Reactの関数コンポーネントなので必ずパスカルケースとしてください。また、同様に、ファイルの名前もパスカルケースとしてください。

import React from "react";
import { Todo } from "./types";

type Props = {
  todos: Todo[];
};

const TodoList = (props: Props) => {
  const todos = props.todos;
  return (
    <div className="space-y-1">
      <div>
        {todos[0].name} 優先度: {todos[0].priority}
      </div>
      <div>
        {todos[1].name} 優先度: {todos[1].priority}
      </div>
      <div>
        {todos[2].name} 優先度: {todos[2].priority}
      </div>
    </div>
  );
};

export default TodoList;

このコンポーネントは、Todo型の配列 todosProps (プロップス) として受け取り、先頭から3件分のTodoの「名前」を「優先度」を表示しています。なお、Reactの文脈において Props とは Properties の略称で、コンポーネントが受け取る「引数」のようなもの と考えてください。

なお、ここでの 第12行目 から 第20行目 のコードは酷いものですが、これは後で書き換えします。

8.4 Appコンポーネントの編集

src/App.tsx を次のように編集してください。第04行目 ではさきほど作成したウェルカムメッセージのコンポーネント (src/WelcomeMessage.tsx) をインポートしています。同様に 第05行目 では、Todoリストのコンポーネント(src/TodoList.tsx) をインポートしています。

また、第03行目では、Todoリストの初期データとして initTodos をインポートしています。なお、インポートする際に 波括弧 { } をつけてインポートしているケースと、そうでないケースがあること に気づくと思います。これらの理由については生成AIで解決してください。

(プロンプト例)

Node.js 環境で TypeScript の勉強しています。サンプルコードを見ていると、ライブラリから関数や変数をインポートする際に、波括弧 {} を付けているケースと、そうではないケースがあることに気づきました。違いについて教えてください。

import { useState } from "react";
import { Todo } from "./types";
import { initTodos } from "./initTodos";
import WelcomeMessage from "./WelcomeMessage";
import TodoList from "./TodoList";

const App = () => {
  const [todos, setTodos] = useState<Todo[]>(initTodos);
  const uncompletedCount = initTodos.filter(
    (todo: Todo) => !todo.isDone
  ).length; // 未完了タスクの数え上げ
  console.log(JSON.stringify(todos, null, 2));

  return (
    <div className="mx-4 mt-10 max-w-2xl md:mx-auto">
      <h1 className="mb-4 text-2xl font-bold">TodoApp</h1>
      <div className="mb-4">
        <WelcomeMessage
          name="寝屋川タヌキ"
          uncompletedCount={uncompletedCount}
        />
      </div>
      <TodoList todos={todos} />
    </div>
  );
};

export default App;

編集後、保存してから 開発モード でアプリを実行してください (npm run dev)。次のような実行結果が得られると思います。さらに、ブラウザで [F12] を押下して デベロッパーツール を起動してコンソール出力 (第12行目 で行われている initTodos の出力) も確認してください。

img

src/App.tsxsrc/TodoList.tsx について、生成AIなどを活用して読解・理解をしてください。特に、しっかりと理解してほしい箇所は次のとおりです。

8.5 TodoListコンポーネントのブラッシュアップ

現状の src/TodoList.tsx は、todos の要素数に関わらず 固定で「3件」のTodoしか表示すること ができません。しかも、todos の要素数が 2個以下のときは実行時エラー が生じます (Todoが1個も表示されなくなります)。

そこで、前回講義で学んだ mapメソッド を利用して、todos の要素数にあわせて表示するように変えたいと思います。この mapメソッドを使うテクニックは Reactでは定番中の定番処理 となるので確実に読解かつ記述できるようにしてください。

import React from "react";
import { Todo } from "./types";

type Props = {
  todos: Todo[];
};

const TodoList = (props: Props) => {
  const todos = props.todos;
  return (
    <div className="space-y-1">
      <div>
        {todos[0].name} 優先度: {todos[0].priority}
      </div>
      <div>
        {todos[1].name} 優先度: {todos[1].priority}
      </div>
      <div>
        {todos[2].name} 優先度: {todos[2].priority}
      </div>
    </div>
  );
};

export default TodoList;

上記のプログラムを次のように書き換えます。前回講義のオブジェクト型の配列に対するmapの適用を参考にしながら、この書き換えを理解してください。

import React from "react";
import { Todo } from "./types";

type Props = {
  todos: Todo[];
};

const TodoList = (props: Props) => {
  const todos = props.todos;

  if (todos.length === 0) {
    return (
      <div className="text-red-500">
        現在、登録されているタスクはありません。
      </div>
    );
  }

  return (
    <div className="space-y-1">
      {todos.map((todo) => (
        <div key={todo.id}>
          {todo.name} 優先度: {todo.priority}
        </div>
      ))}
    </div>
  );
};

export default TodoList;

書き換え後のプログラムでは、todos空配列のとき (todos.length === 0 のとき) には、Todoリストの代わりに「現在、登録されているタスクはありません。」というメッセージを表示するようにしています。

また、map の戻り値となる一番外側のJSX要素 (この例では <div> 要素) については、第22行目 のように 必ず key 属性を設定してくださいkey の値は ユニークな値 (配列内の他要素と重複のない一意な値) であれば何であっても問題ありません。例えば、Todoの「名前 (name)」が絶対に重複しないのであれば <div key={todo.name}> としても問題ありません。

key 属性を設定していないと、実行時に Each child in a list should have a unique "key" prop という警告が表示されます。また、再レンダリングに際して意図した動作をしなくなる可能性があります。詳しい原理は生成AIで解決してください。

(プロンプト例)

Reactを使ったウェブアプリを開発しています。配列のmapメソッドを使用してJSXを構成する際、key属性が要求されるのは何故ですか。Reactは何のためにkey属性を必要としますか。

8.5.1 演習

前回講義で学んだように、Todoの並び替えは「配列のsortメソッド」を利用して行なうことができます。例えば、優先度を昇順 (1 \(\to\) 2 \(\to\) 3の順) にしたいときは、第09行目const todos = props.todos; を以下のように書き換えます。

const todos = [...props.todos].sort((a, b) => a.priority - b.priority);

前回講義の複数のソートキーを使ったオブジェクト配列の並び替えを参考にして、第1ソートキーisDone (未完了を先に表示)、第2ソートキーdeadline として並び替えたTodoリストが表示されるようにしてください。

9 演習 (宿題: 120分~)

主に src/TodoList.tsxJSX (HTML)Tailwind CSS に編集を加えてTodoリストコンポーネントの表示を 自分流にカスタイマイズ してみてください。これは、UI/UXの設計練習も兼ねています。また、後期前半の大課題に関連する内容にもなってきます。

なお、現時点では「チェックボックス」や「ボタン」などの要素追加やスタイリングは不要です (これらの内容は次回講義で扱います)。

(Todoリストのカスタマイズの例)

img

取組みに際しては、生成AIを賢く活用してください (コード生成ツールとして使うのではなく、内容の理解のためのツールとして使用してください)。

(プロンプト例)・再掲

TailwindCSS で、次のようにスタイルが適用されていました。各CSSクラスの意味について解説してください。
<div className="mx-4 md:mx-auto mt-10 max-w-2xl">

(プロンプト例)・再掲

TailwindCSS でボタンに「ドロップシャドウ」を適用したいです。どのようにすればよいですか。
<button className="bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded-md">ほげ</button>